gglib_core/ports/
mcp_dto.rs

1//! MCP DTOs for cross-boundary communication (Tauri, Axum, TypeScript).
2//!
3//! These types are designed to be serde-stable and avoid internal implementation
4//! details like `PathBuf` crossing the wire.
5
6use serde::{Deserialize, Serialize};
7
8/// Result of executable path resolution for a server.
9///
10/// This is the contract between backend (Rust) and frontend (TypeScript/Axum clients).
11/// All fields are String-based to ensure clean JSON serialization.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub struct ResolutionStatus {
15    /// Whether resolution succeeded.
16    pub success: bool,
17
18    /// The resolved absolute path (if successful).
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub resolved_path: Option<String>,
21
22    /// All attempts made during resolution (for diagnostics).
23    #[serde(skip_serializing_if = "Vec::is_empty", default)]
24    pub attempts: Vec<ResolutionAttempt>,
25
26    /// Non-fatal warnings.
27    #[serde(skip_serializing_if = "Vec::is_empty", default)]
28    pub warnings: Vec<String>,
29
30    /// Error message (if resolution failed).
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub error_message: Option<String>,
33
34    /// Suggested command to run to find the executable (e.g., "command -v npx").
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub suggested_fix: Option<String>,
37}
38
39/// A single resolution attempt for diagnostics.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub struct ResolutionAttempt {
43    /// The candidate path that was tried.
44    pub candidate: String,
45
46    /// The outcome of checking this candidate (simple string for cross-language compat).
47    pub outcome: String,
48}
49
50impl ResolutionStatus {
51    /// Get a user-friendly error message with suggestions.
52    pub fn error_message_with_suggestions(&self) -> String {
53        let base_msg = self
54            .error_message
55            .clone()
56            .unwrap_or_else(|| "Resolution failed".to_string());
57
58        if self.attempts.is_empty() {
59            return base_msg;
60        }
61
62        let attempts_list: Vec<String> = self
63            .attempts
64            .iter()
65            .map(|a| format!("  ✗ {}: {}", a.candidate, a.outcome))
66            .collect();
67
68        let mut msg = format!("{}\n\nTried:\n{}", base_msg, attempts_list.join("\n"));
69
70        if let Some(fix) = &self.suggested_fix {
71            msg.push_str("\n\nSuggested fix: ");
72            msg.push_str(fix);
73        }
74
75        // Add install hint if all attempts are NotFound
76        let all_not_found = self
77            .attempts
78            .iter()
79            .all(|a| a.outcome.contains("not found"));
80        if all_not_found {
81            msg.push_str("\n\n• Install Node.js if not installed");
82        }
83
84        msg
85    }
86}