1use serde::Serialize;
10use serde_json::json;
11use thiserror::Error;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
19pub enum McpErrorKind {
20 ParseError,
22 InvalidRequest,
23 MethodNotFound,
24 InvalidParams,
25 InternalError,
26 AgentNotRunning,
28 ProfileNotFound,
29 KeyOutOfScope,
30 KeyMissing,
31 RunTimeout,
32 RevealDisabled,
33 BiometricDenied,
34 ScopeWidening,
35 InstallTargetUnknown,
36 InstallConfigMalformed,
37}
38
39impl McpErrorKind {
40 pub fn code(&self) -> i32 {
42 match self {
43 McpErrorKind::ParseError => -32_700,
44 McpErrorKind::InvalidRequest => -32_600,
45 McpErrorKind::MethodNotFound => -32_601,
46 McpErrorKind::InvalidParams => -32_602,
47 McpErrorKind::InternalError => -32_603,
48 McpErrorKind::AgentNotRunning => -32_001,
49 McpErrorKind::ProfileNotFound => -32_002,
50 McpErrorKind::KeyOutOfScope => -32_003,
51 McpErrorKind::KeyMissing => -32_004,
52 McpErrorKind::RunTimeout => -32_005,
53 McpErrorKind::RevealDisabled => -32_006,
54 McpErrorKind::BiometricDenied => -32_007,
55 McpErrorKind::ScopeWidening => -32_008,
56 McpErrorKind::InstallTargetUnknown => -32_009,
57 McpErrorKind::InstallConfigMalformed => -32_010,
58 }
59 }
60
61 pub fn default_message(&self) -> &'static str {
63 match self {
64 McpErrorKind::ParseError => "Invalid JSON-RPC frame",
65 McpErrorKind::InvalidRequest => "Invalid request: missing required fields",
66 McpErrorKind::MethodNotFound => "Method not found",
67 McpErrorKind::InvalidParams => "Invalid parameters",
68 McpErrorKind::InternalError => "Internal server error",
69 McpErrorKind::AgentNotRunning => {
70 "tsafe-agent not running. Run `tsafe agent unlock --profile <p>` and reload the host."
71 }
72 McpErrorKind::ProfileNotFound => "Profile not found",
73 McpErrorKind::KeyOutOfScope => {
74 "Key is outside the configured scope for this server"
75 }
76 McpErrorKind::KeyMissing => "Key not found in profile",
77 McpErrorKind::RunTimeout => "Command exceeded timeout",
78 McpErrorKind::RevealDisabled => {
79 "tsafe_reveal is not enabled on this server. Restart with --allow-reveal."
80 }
81 McpErrorKind::BiometricDenied => "Biometric verification declined",
82 McpErrorKind::ScopeWidening => {
83 "Request-time scope or profile widening is not allowed"
84 }
85 McpErrorKind::InstallTargetUnknown => {
86 "Unknown host. Supported: claude, cursor, continue, windsurf, codex"
87 }
88 McpErrorKind::InstallConfigMalformed => "Existing host config is not valid",
89 }
90 }
91}
92
93#[derive(Debug, Clone, Error)]
99#[error("{message}")]
100pub struct McpError {
101 pub kind: McpErrorKind,
102 pub code: i32,
103 pub message: String,
104 pub data: Option<serde_json::Value>,
105}
106
107impl McpError {
108 pub fn new<S: Into<String>>(kind: McpErrorKind, detail: S) -> Self {
111 let detail = detail.into();
112 let base = kind.default_message();
113 let message = if detail.is_empty() {
114 base.to_string()
115 } else if detail.starts_with(base) {
116 detail
118 } else {
119 format!("{base}: {detail}")
120 };
121 Self {
122 kind,
123 code: kind.code(),
124 message,
125 data: None,
126 }
127 }
128
129 pub fn kind_only(kind: McpErrorKind) -> Self {
132 Self {
133 kind,
134 code: kind.code(),
135 message: kind.default_message().to_string(),
136 data: None,
137 }
138 }
139
140 pub fn with_data(mut self, data: serde_json::Value) -> Self {
143 self.data = Some(data);
144 self
145 }
146
147 pub fn to_rpc_error_object(&self) -> serde_json::Value {
149 match &self.data {
150 Some(d) => json!({ "code": self.code, "message": self.message, "data": d }),
151 None => json!({ "code": self.code, "message": self.message }),
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn every_kind_returns_the_design_doc_code() {
162 let pairs: &[(McpErrorKind, i32)] = &[
163 (McpErrorKind::ParseError, -32_700),
164 (McpErrorKind::InvalidRequest, -32_600),
165 (McpErrorKind::MethodNotFound, -32_601),
166 (McpErrorKind::InvalidParams, -32_602),
167 (McpErrorKind::InternalError, -32_603),
168 (McpErrorKind::AgentNotRunning, -32_001),
169 (McpErrorKind::ProfileNotFound, -32_002),
170 (McpErrorKind::KeyOutOfScope, -32_003),
171 (McpErrorKind::KeyMissing, -32_004),
172 (McpErrorKind::RunTimeout, -32_005),
173 (McpErrorKind::RevealDisabled, -32_006),
174 (McpErrorKind::BiometricDenied, -32_007),
175 (McpErrorKind::ScopeWidening, -32_008),
176 (McpErrorKind::InstallTargetUnknown, -32_009),
177 (McpErrorKind::InstallConfigMalformed, -32_010),
178 ];
179 for (kind, code) in pairs {
180 assert_eq!(kind.code(), *code, "kind {kind:?}");
181 let err = McpError::kind_only(*kind);
182 assert_eq!(err.code, *code);
183 }
184 }
185
186 #[test]
187 fn rpc_error_object_includes_data_when_present() {
188 let err = McpError::new(McpErrorKind::KeyOutOfScope, "key 'demo/foo'")
189 .with_data(json!({"key": "demo/foo"}));
190 let obj = err.to_rpc_error_object();
191 assert_eq!(obj["code"], -32_003);
192 assert!(obj["message"].as_str().unwrap().contains("demo/foo"));
193 assert_eq!(obj["data"]["key"], "demo/foo");
194 }
195
196 #[test]
197 fn default_messages_match_design_5_4() {
198 assert!(McpErrorKind::AgentNotRunning
199 .default_message()
200 .contains("tsafe-agent not running"));
201 assert!(McpErrorKind::RevealDisabled
202 .default_message()
203 .contains("--allow-reveal"));
204 assert!(McpErrorKind::InstallTargetUnknown
205 .default_message()
206 .contains("claude, cursor, continue, windsurf, codex"));
207 assert!(McpErrorKind::ScopeWidening
208 .default_message()
209 .contains("scope or profile widening"));
210 }
211}