Skip to main content

vtcode_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    fn code(&self) -> String {
38        format!("MCP_E{:03}", *self as u32)
39    }
40
41    /// Get human-readable error name
42    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 => "Check that the tool name is correct and the MCP provider is running.",
58            Self::ToolInvocationFailed => {
59                "The MCP tool returned an error. Check the tool's arguments and provider logs."
60            }
61            Self::ProviderNotFound => {
62                "Verify the provider name in vtcode.toml or .mcp.json matches a configured MCP server."
63            }
64            Self::ProviderUnavailable => "The MCP server may be down. Check that the command/endpoint is reachable.",
65            Self::SchemaInvalid => {
66                "The tool's input schema does not match expected format. Check the MCP server implementation."
67            }
68            Self::ConfigurationError => "Review the MCP section of vtcode.toml or .mcp.json for syntax errors.",
69            Self::InitializationTimeout => {
70                "The MCP server took too long to start. Increase startup_timeout_ms or check the server process."
71            }
72        }
73    }
74}
75
76impl fmt::Display for ErrorCode {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(f, "{}", self.code())
79    }
80}
81
82/// Helper to create a "tool not found" error
83///
84/// Error Code: MCP_E001
85pub fn tool_not_found(name: &str) -> anyhow::Error {
86    anyhow!("[{}] MCP tool '{}' not found", ErrorCode::ToolNotFound.code(), name)
87}
88
89/// Helper to create a "provider not found" error
90///
91/// Error Code: MCP_E011
92pub fn provider_not_found(name: &str) -> anyhow::Error {
93    anyhow!("[{}] MCP provider '{}' not found", ErrorCode::ProviderNotFound.code(), name)
94}
95
96/// Helper to create a "provider unavailable" error
97///
98/// Error Code: MCP_E012
99pub fn provider_unavailable(name: &str) -> anyhow::Error {
100    anyhow!(
101        "[{}] MCP provider '{}' is unavailable or failed to initialize",
102        ErrorCode::ProviderUnavailable.code(),
103        name
104    )
105}
106
107/// Helper to create a "schema invalid" error
108///
109/// Error Code: MCP_E021
110pub fn schema_invalid(reason: &str) -> anyhow::Error {
111    anyhow!("[{}] MCP tool schema is invalid: {}", ErrorCode::SchemaInvalid.code(), reason)
112}
113
114/// Helper to create a "tool invocation failed" error
115///
116/// Error Code: MCP_E002
117pub fn tool_invocation_failed(provider: &str, tool: &str, reason: &str) -> anyhow::Error {
118    anyhow!(
119        "[{}] Failed to invoke tool '{}' on provider '{}': {}",
120        ErrorCode::ToolInvocationFailed.code(),
121        tool,
122        provider,
123        reason
124    )
125}
126
127/// Helper to create an "initialization timeout" error
128///
129/// Error Code: MCP_E032
130pub fn initialization_timeout(timeout_secs: u64) -> anyhow::Error {
131    anyhow!(
132        "[{}] MCP initialization timeout after {} seconds",
133        ErrorCode::InitializationTimeout.code(),
134        timeout_secs
135    )
136}
137
138/// Helper to create a "configuration error"
139///
140/// Error Code: MCP_E031
141pub fn configuration_error(reason: &str) -> anyhow::Error {
142    anyhow!("[{}] MCP configuration error: {}", ErrorCode::ConfigurationError.code(), reason)
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_error_codes_format() {
151        assert_eq!(ErrorCode::ToolNotFound.code(), "MCP_E001");
152        assert_eq!(ErrorCode::ToolInvocationFailed.code(), "MCP_E002");
153        assert_eq!(ErrorCode::ProviderNotFound.code(), "MCP_E011");
154        assert_eq!(ErrorCode::ProviderUnavailable.code(), "MCP_E012");
155        assert_eq!(ErrorCode::SchemaInvalid.code(), "MCP_E021");
156        assert_eq!(ErrorCode::ConfigurationError.code(), "MCP_E031");
157        assert_eq!(ErrorCode::InitializationTimeout.code(), "MCP_E032");
158    }
159
160    #[test]
161    fn test_error_names() {
162        assert_eq!(ErrorCode::ToolNotFound.name(), "ToolNotFound");
163        assert_eq!(ErrorCode::ProviderNotFound.name(), "ProviderNotFound");
164        assert_eq!(ErrorCode::InitializationTimeout.name(), "InitializationTimeout");
165    }
166
167    #[test]
168    fn test_error_messages_with_codes() {
169        let err = tool_not_found("missing_tool");
170        let msg = err.to_string();
171        assert!(msg.contains("[MCP_E001]"));
172        assert!(msg.contains("missing_tool"));
173        assert!(msg.contains("not found"));
174
175        let err = provider_not_found("missing_provider");
176        let msg = err.to_string();
177        assert!(msg.contains("[MCP_E011]"));
178        assert!(msg.contains("missing_provider"));
179
180        let err = initialization_timeout(15);
181        let msg = err.to_string();
182        assert!(msg.contains("[MCP_E032]"));
183        assert!(msg.contains("15 seconds"));
184
185        let err = tool_invocation_failed("claude", vtcode_config::constants::tools::LIST_FILES, "timeout");
186        let msg = err.to_string();
187        assert!(msg.contains("[MCP_E002]"));
188        assert!(msg.contains(vtcode_config::constants::tools::LIST_FILES));
189        assert!(msg.contains("timeout"));
190    }
191
192    #[test]
193    fn test_error_code_display() {
194        assert_eq!(ErrorCode::ToolNotFound.to_string(), "MCP_E001");
195        assert_eq!(ErrorCode::ProviderUnavailable.to_string(), "MCP_E012");
196    }
197}