1use std::collections::BTreeMap;
2use std::io;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value as JsonValue;
6
7use crate::config::McpTransport;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(untagged)]
11pub enum JsonRpcId {
12 Number(u64),
13 String(String),
14 Null,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub struct JsonRpcRequest<T = JsonValue> {
19 pub jsonrpc: String,
20 pub id: JsonRpcId,
21 pub method: String,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub params: Option<T>,
24}
25
26impl<T> JsonRpcRequest<T> {
27 #[must_use]
28 pub fn new(id: JsonRpcId, method: impl Into<String>, params: Option<T>) -> Self {
29 Self {
30 jsonrpc: "2.0".to_string(),
31 id,
32 method: method.into(),
33 params,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub struct JsonRpcError {
40 pub code: i64,
41 pub message: String,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub data: Option<JsonValue>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47pub struct JsonRpcResponse<T = JsonValue> {
48 pub jsonrpc: String,
49 pub id: JsonRpcId,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub result: Option<T>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub error: Option<JsonRpcError>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57#[serde(rename_all = "camelCase")]
58pub struct McpInitializeParams {
59 pub protocol_version: String,
60 pub capabilities: JsonValue,
61 pub client_info: McpInitializeClientInfo,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(rename_all = "camelCase")]
66pub struct McpInitializeClientInfo {
67 pub name: String,
68 pub version: String,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72#[serde(rename_all = "camelCase")]
73pub struct McpInitializeResult {
74 pub protocol_version: String,
75 pub capabilities: JsonValue,
76 pub server_info: McpInitializeServerInfo,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
80#[serde(rename_all = "camelCase")]
81pub struct McpInitializeServerInfo {
82 pub name: String,
83 pub version: String,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
87#[serde(rename_all = "camelCase")]
88pub struct McpListToolsParams {
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub cursor: Option<String>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
94pub struct McpTool {
95 pub name: String,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub description: Option<String>,
98 #[serde(rename = "inputSchema", skip_serializing_if = "Option::is_none")]
99 pub input_schema: Option<JsonValue>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub annotations: Option<JsonValue>,
102 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
103 pub meta: Option<JsonValue>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107#[serde(rename_all = "camelCase")]
108pub struct McpListToolsResult {
109 pub tools: Vec<McpTool>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub next_cursor: Option<String>,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
115#[serde(rename_all = "camelCase")]
116pub struct McpToolCallParams {
117 pub name: String,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub arguments: Option<JsonValue>,
120 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
121 pub meta: Option<JsonValue>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
125pub struct McpToolCallContent {
126 #[serde(rename = "type")]
127 pub kind: String,
128 #[serde(flatten)]
129 pub data: BTreeMap<String, JsonValue>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
133#[serde(rename_all = "camelCase")]
134pub struct McpToolCallResult {
135 #[serde(default)]
136 pub content: Vec<McpToolCallContent>,
137 #[serde(default)]
138 pub structured_content: Option<JsonValue>,
139 #[serde(default)]
140 pub is_error: Option<bool>,
141 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
142 pub meta: Option<JsonValue>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
146#[serde(rename_all = "camelCase")]
147pub struct McpListResourcesParams {
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub cursor: Option<String>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
153pub struct McpResource {
154 pub uri: String,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub name: Option<String>,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub description: Option<String>,
159 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
160 pub mime_type: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub annotations: Option<JsonValue>,
163 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
164 pub meta: Option<JsonValue>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
168#[serde(rename_all = "camelCase")]
169pub struct McpListResourcesResult {
170 pub resources: Vec<McpResource>,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub next_cursor: Option<String>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
176#[serde(rename_all = "camelCase")]
177pub struct McpReadResourceParams {
178 pub uri: String,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
182pub struct McpResourceContents {
183 pub uri: String,
184 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
185 pub mime_type: Option<String>,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub text: Option<String>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub blob: Option<String>,
190 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
191 pub meta: Option<JsonValue>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
195pub struct McpReadResourceResult {
196 pub contents: Vec<McpResourceContents>,
197}
198
199#[derive(Debug, Clone, PartialEq)]
200pub struct ManagedMcpTool {
201 pub server_name: String,
202 pub qualified_name: String,
203 pub raw_name: String,
204 pub tool: McpTool,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct UnsupportedMcpServer {
209 pub server_name: String,
210 pub transport: McpTransport,
211 pub reason: String,
212}
213
214#[derive(Debug)]
215pub enum McpServerManagerError {
216 Io(io::Error),
217 SpawnFailed {
218 server_name: String,
219 source: io::Error,
220 },
221 JsonRpc {
222 server_name: String,
223 method: &'static str,
224 error: JsonRpcError,
225 },
226 InvalidResponse {
227 server_name: String,
228 method: &'static str,
229 details: String,
230 },
231 UnknownTool {
232 qualified_name: String,
233 },
234 UnknownServer {
235 server_name: String,
236 },
237}
238
239impl std::fmt::Display for McpServerManagerError {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 match self {
242 Self::Io(error) => write!(f, "{error}"),
243 Self::SpawnFailed {
244 server_name,
245 source,
246 } => write!(
247 f,
248 "failed to connect to MCP server `{server_name}`: {source}"
249 ),
250 Self::JsonRpc {
251 server_name,
252 method,
253 error,
254 } => write!(
255 f,
256 "MCP server `{server_name}` returned JSON-RPC error for {method}: {} ({})",
257 error.message, error.code
258 ),
259 Self::InvalidResponse {
260 server_name,
261 method,
262 details,
263 } => write!(
264 f,
265 "MCP server `{server_name}` returned invalid response for {method}: {details}"
266 ),
267 Self::UnknownTool { qualified_name } => {
268 write!(f, "unknown MCP tool `{qualified_name}`")
269 }
270 Self::UnknownServer { server_name } => write!(f, "unknown MCP server `{server_name}`"),
271 }
272 }
273}
274
275impl std::error::Error for McpServerManagerError {
276 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
277 match self {
278 Self::Io(error) | Self::SpawnFailed { source: error, .. } => Some(error),
279 Self::JsonRpc { .. }
280 | Self::InvalidResponse { .. }
281 | Self::UnknownTool { .. }
282 | Self::UnknownServer { .. } => None,
283 }
284 }
285}
286
287impl From<io::Error> for McpServerManagerError {
288 fn from(value: io::Error) -> Self {
289 Self::Io(value)
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn error_display_covers_all_variants() {
299 let io_err = McpServerManagerError::Io(io::Error::new(io::ErrorKind::NotFound, "gone"));
300 assert!(io_err.to_string().contains("gone"));
301
302 let spawn_err = McpServerManagerError::SpawnFailed {
303 server_name: "test-srv".into(),
304 source: io::Error::new(io::ErrorKind::PermissionDenied, "denied"),
305 };
306 assert!(spawn_err.to_string().contains("test-srv"));
307 assert!(spawn_err.to_string().contains("denied"));
308
309 let rpc_err = McpServerManagerError::JsonRpc {
310 server_name: "rpc-srv".into(),
311 method: "initialize",
312 error: JsonRpcError {
313 code: -32600,
314 message: "bad request".into(),
315 data: None,
316 },
317 };
318 assert!(rpc_err.to_string().contains("rpc-srv"));
319 assert!(rpc_err.to_string().contains("bad request"));
320
321 let invalid = McpServerManagerError::InvalidResponse {
322 server_name: "inv-srv".into(),
323 method: "tools/list",
324 details: "missing tools".into(),
325 };
326 assert!(invalid.to_string().contains("inv-srv"));
327 assert!(invalid.to_string().contains("missing tools"));
328
329 let unknown_tool = McpServerManagerError::UnknownTool {
330 qualified_name: "srv__tool".into(),
331 };
332 assert!(unknown_tool.to_string().contains("srv__tool"));
333
334 let unknown_srv = McpServerManagerError::UnknownServer {
335 server_name: "missing".into(),
336 };
337 assert!(unknown_srv.to_string().contains("missing"));
338 }
339
340 #[test]
341 fn error_source_returns_io_for_io_and_spawn_variants() {
342 let io_err = McpServerManagerError::Io(io::Error::other("x"));
343 assert!(std::error::Error::source(&io_err).is_some());
344
345 let spawn_err = McpServerManagerError::SpawnFailed {
346 server_name: "s".into(),
347 source: io::Error::other("y"),
348 };
349 assert!(std::error::Error::source(&spawn_err).is_some());
350
351 let rpc_err = McpServerManagerError::JsonRpc {
352 server_name: "s".into(),
353 method: "m",
354 error: JsonRpcError {
355 code: 0,
356 message: String::new(),
357 data: None,
358 },
359 };
360 assert!(std::error::Error::source(&rpc_err).is_none());
361 }
362}