Skip to main content

vtcode_core/mcp/
errors.rs

1/// Unified error handling for MCP operations
2///
3/// VT Code uses `anyhow::Result<T>` for all MCP errors to maintain consistency
4/// with the Rust SDK patterns and provide rich error context.
5///
6/// Phase 3: Error codes follow the pattern MCP_E{code}
7/// - MCP_E001-E010: Tool-related errors
8/// - MCP_E011-E020: Provider-related errors
9/// - MCP_E021-E030: Schema-related errors
10/// - MCP_E031-E040: Configuration-related errors
11use anyhow::anyhow;
12use std::fmt;
13
14pub type McpResult<T> = anyhow::Result<T>;
15
16/// MCP Error codes for better error identification and debugging
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ErrorCode {
19    /// MCP_E001: Tool not found
20    ToolNotFound = 1,
21    /// MCP_E002: Tool invocation failed
22    ToolInvocationFailed = 2,
23    /// MCP_E011: Provider not found
24    ProviderNotFound = 11,
25    /// MCP_E012: Provider unavailable
26    ProviderUnavailable = 12,
27    /// MCP_E021: Schema validation failed
28    SchemaInvalid = 21,
29    /// MCP_E031: Configuration error
30    ConfigurationError = 31,
31    /// MCP_E032: Initialization timeout
32    InitializationTimeout = 32,
33}
34
35impl ErrorCode {
36    /// Get error code string (e.g., "MCP_E001")
37    pub fn code(&self) -> String {
38        format!("MCP_E{:03}", *self as u32)
39    }
40
41    /// Get human-readable error name
42    pub fn name(&self) -> &'static str {
43        match self {
44            Self::ToolNotFound => "ToolNotFound",
45            Self::ToolInvocationFailed => "ToolInvocationFailed",
46            Self::ProviderNotFound => "ProviderNotFound",
47            Self::ProviderUnavailable => "ProviderUnavailable",
48            Self::SchemaInvalid => "SchemaInvalid",
49            Self::ConfigurationError => "ConfigurationError",
50            Self::InitializationTimeout => "InitializationTimeout",
51        }
52    }
53
54    /// Returns a short, actionable guidance message suitable for display in the TUI.
55    pub fn user_guidance(&self) -> &'static str {
56        match self {
57            Self::ToolNotFound => {
58                "Check that the tool name is correct and the MCP provider is running."
59            }
60            Self::ToolInvocationFailed => {
61                "The MCP tool returned an error. Check the tool's arguments and provider logs."
62            }
63            Self::ProviderNotFound => {
64                "Verify the provider name in vtcode.toml or .mcp.json matches a configured MCP server."
65            }
66            Self::ProviderUnavailable => {
67                "The MCP server may be down. Check that the command/endpoint is reachable."
68            }
69            Self::SchemaInvalid => {
70                "The tool's input schema does not match expected format. Check the MCP server implementation."
71            }
72            Self::ConfigurationError => {
73                "Review the MCP section of vtcode.toml or .mcp.json for syntax errors."
74            }
75            Self::InitializationTimeout => {
76                "The MCP server took too long to start. Increase startup_timeout_ms or check the server process."
77            }
78        }
79    }
80}
81
82impl fmt::Display for ErrorCode {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "{}", self.code())
85    }
86}
87
88/// Helper to create a "tool not found" error
89///
90/// Error Code: MCP_E001
91pub fn tool_not_found(name: &str) -> anyhow::Error {
92    anyhow!(
93        "[{}] MCP tool '{}' not found",
94        ErrorCode::ToolNotFound.code(),
95        name
96    )
97}
98
99/// Helper to create a "provider not found" error
100///
101/// Error Code: MCP_E011
102pub fn provider_not_found(name: &str) -> anyhow::Error {
103    anyhow!(
104        "[{}] MCP provider '{}' not found",
105        ErrorCode::ProviderNotFound.code(),
106        name
107    )
108}
109
110/// Helper to create a "provider unavailable" error
111///
112/// Error Code: MCP_E012
113pub fn provider_unavailable(name: &str) -> anyhow::Error {
114    anyhow!(
115        "[{}] MCP provider '{}' is unavailable or failed to initialize",
116        ErrorCode::ProviderUnavailable.code(),
117        name
118    )
119}
120
121/// Helper to create a "schema invalid" error
122///
123/// Error Code: MCP_E021
124pub fn schema_invalid(reason: &str) -> anyhow::Error {
125    anyhow!(
126        "[{}] MCP tool schema is invalid: {}",
127        ErrorCode::SchemaInvalid.code(),
128        reason
129    )
130}
131
132/// Helper to create a "tool invocation failed" error
133///
134/// Error Code: MCP_E002
135pub fn tool_invocation_failed(provider: &str, tool: &str, reason: &str) -> anyhow::Error {
136    anyhow!(
137        "[{}] Failed to invoke tool '{}' on provider '{}': {}",
138        ErrorCode::ToolInvocationFailed.code(),
139        tool,
140        provider,
141        reason
142    )
143}
144
145/// Helper to create an "initialization timeout" error
146///
147/// Error Code: MCP_E032
148pub fn initialization_timeout(timeout_secs: u64) -> anyhow::Error {
149    anyhow!(
150        "[{}] MCP initialization timeout after {} seconds",
151        ErrorCode::InitializationTimeout.code(),
152        timeout_secs
153    )
154}
155
156/// Helper to create a "configuration error"
157///
158/// Error Code: MCP_E031
159pub fn configuration_error(reason: &str) -> anyhow::Error {
160    anyhow!(
161        "[{}] MCP configuration error: {}",
162        ErrorCode::ConfigurationError.code(),
163        reason
164    )
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_error_codes_format() {
173        assert_eq!(ErrorCode::ToolNotFound.code(), "MCP_E001");
174        assert_eq!(ErrorCode::ToolInvocationFailed.code(), "MCP_E002");
175        assert_eq!(ErrorCode::ProviderNotFound.code(), "MCP_E011");
176        assert_eq!(ErrorCode::ProviderUnavailable.code(), "MCP_E012");
177        assert_eq!(ErrorCode::SchemaInvalid.code(), "MCP_E021");
178        assert_eq!(ErrorCode::ConfigurationError.code(), "MCP_E031");
179        assert_eq!(ErrorCode::InitializationTimeout.code(), "MCP_E032");
180    }
181
182    #[test]
183    fn test_error_names() {
184        assert_eq!(ErrorCode::ToolNotFound.name(), "ToolNotFound");
185        assert_eq!(ErrorCode::ProviderNotFound.name(), "ProviderNotFound");
186        assert_eq!(
187            ErrorCode::InitializationTimeout.name(),
188            "InitializationTimeout"
189        );
190    }
191
192    #[test]
193    fn test_error_messages_with_codes() {
194        let err = tool_not_found("missing_tool");
195        let msg = err.to_string();
196        assert!(msg.contains("[MCP_E001]"));
197        assert!(msg.contains("missing_tool"));
198        assert!(msg.contains("not found"));
199
200        let err = provider_not_found("missing_provider");
201        let msg = err.to_string();
202        assert!(msg.contains("[MCP_E011]"));
203        assert!(msg.contains("missing_provider"));
204
205        let err = initialization_timeout(15);
206        let msg = err.to_string();
207        assert!(msg.contains("[MCP_E032]"));
208        assert!(msg.contains("15 seconds"));
209
210        let err = tool_invocation_failed(
211            "claude",
212            vtcode_config::constants::tools::LIST_FILES,
213            "timeout",
214        );
215        let msg = err.to_string();
216        assert!(msg.contains("[MCP_E002]"));
217        assert!(msg.contains(vtcode_config::constants::tools::LIST_FILES));
218        assert!(msg.contains("timeout"));
219    }
220
221    #[test]
222    fn test_error_code_display() {
223        assert_eq!(ErrorCode::ToolNotFound.to_string(), "MCP_E001");
224        assert_eq!(ErrorCode::ProviderUnavailable.to_string(), "MCP_E012");
225    }
226}