Skip to main content

codineer_runtime/mcp_stdio/
types.rs

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}