Skip to main content

a3s_code_core/mcp/
protocol.rs

1//! MCP Protocol Type Definitions
2//!
3//! Defines the core types for the Model Context Protocol (MCP).
4//! Based on the MCP specification: <https://spec.modelcontextprotocol.io/>
5
6use serde::{Deserialize, Deserializer, Serialize};
7use std::collections::HashMap;
8
9/// MCP protocol version
10pub const PROTOCOL_VERSION: &str = "2024-11-05";
11
12/// JSON-RPC request
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct JsonRpcRequest {
15    pub jsonrpc: String,
16    pub id: u64,
17    pub method: String,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub params: Option<serde_json::Value>,
20}
21
22impl JsonRpcRequest {
23    pub fn new(id: u64, method: &str, params: Option<serde_json::Value>) -> Self {
24        Self {
25            jsonrpc: "2.0".to_string(),
26            id,
27            method: method.to_string(),
28            params,
29        }
30    }
31}
32
33/// JSON-RPC response
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct JsonRpcResponse {
36    pub jsonrpc: String,
37    pub id: Option<u64>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub result: Option<serde_json::Value>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub error: Option<JsonRpcError>,
42}
43
44/// JSON-RPC error
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct JsonRpcError {
47    pub code: i32,
48    pub message: String,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub data: Option<serde_json::Value>,
51}
52
53/// JSON-RPC notification (no id)
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct JsonRpcNotification {
56    pub jsonrpc: String,
57    pub method: String,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub params: Option<serde_json::Value>,
60}
61
62impl JsonRpcNotification {
63    pub fn new(method: &str, params: Option<serde_json::Value>) -> Self {
64        Self {
65            jsonrpc: "2.0".to_string(),
66            method: method.to_string(),
67            params,
68        }
69    }
70}
71
72// ============================================================================
73// MCP Initialize
74// ============================================================================
75
76/// Client capabilities
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78pub struct ClientCapabilities {
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub roots: Option<RootsCapability>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub sampling: Option<SamplingCapability>,
83}
84
85#[derive(Debug, Clone, Default, Serialize, Deserialize)]
86pub struct RootsCapability {
87    #[serde(default)]
88    pub list_changed: bool,
89}
90
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct SamplingCapability {}
93
94/// Client info
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ClientInfo {
97    pub name: String,
98    pub version: String,
99}
100
101/// Initialize request params
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct InitializeParams {
105    pub protocol_version: String,
106    pub capabilities: ClientCapabilities,
107    pub client_info: ClientInfo,
108}
109
110/// Server capabilities
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct ServerCapabilities {
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub tools: Option<ToolsCapability>,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub resources: Option<ResourcesCapability>,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub prompts: Option<PromptsCapability>,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub logging: Option<LoggingCapability>,
121}
122
123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct ToolsCapability {
126    #[serde(default)]
127    pub list_changed: bool,
128}
129
130#[derive(Debug, Clone, Default, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct ResourcesCapability {
133    #[serde(default)]
134    pub subscribe: bool,
135    #[serde(default)]
136    pub list_changed: bool,
137}
138
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct PromptsCapability {
142    #[serde(default)]
143    pub list_changed: bool,
144}
145
146#[derive(Debug, Clone, Default, Serialize, Deserialize)]
147pub struct LoggingCapability {}
148
149/// Server info
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ServerInfo {
152    pub name: String,
153    pub version: String,
154}
155
156/// Initialize result
157#[derive(Debug, Clone, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct InitializeResult {
160    pub protocol_version: String,
161    pub capabilities: ServerCapabilities,
162    pub server_info: ServerInfo,
163}
164
165// ============================================================================
166// MCP Tools
167// ============================================================================
168
169/// MCP tool definition
170#[derive(Debug, Clone, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct McpTool {
173    pub name: String,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub description: Option<String>,
176    pub input_schema: serde_json::Value,
177}
178
179/// List tools result
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ListToolsResult {
182    pub tools: Vec<McpTool>,
183}
184
185/// Call tool params
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct CallToolParams {
188    pub name: String,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub arguments: Option<serde_json::Value>,
191}
192
193/// Tool content types
194#[derive(Debug, Clone, Serialize, Deserialize)]
195#[serde(tag = "type", rename_all = "lowercase")]
196pub enum ToolContent {
197    Text {
198        text: String,
199    },
200    Image {
201        data: String,
202        #[serde(rename = "mimeType")]
203        mime_type: String,
204    },
205    Resource {
206        resource: ResourceContent,
207    },
208}
209
210/// Resource content
211#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct ResourceContent {
214    pub uri: String,
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub mime_type: Option<String>,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub text: Option<String>,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub blob: Option<String>,
221}
222
223/// Call tool result
224#[derive(Debug, Clone, Serialize, Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct CallToolResult {
227    pub content: Vec<ToolContent>,
228    #[serde(default)]
229    pub is_error: bool,
230}
231
232// ============================================================================
233// MCP Resources
234// ============================================================================
235
236/// MCP resource definition
237#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct McpResource {
240    pub uri: String,
241    pub name: String,
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub description: Option<String>,
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub mime_type: Option<String>,
246}
247
248/// List resources result
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ListResourcesResult {
251    pub resources: Vec<McpResource>,
252}
253
254/// Read resource params
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct ReadResourceParams {
257    pub uri: String,
258}
259
260/// Read resource result
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct ReadResourceResult {
263    pub contents: Vec<ResourceContent>,
264}
265
266// ============================================================================
267// MCP Prompts
268// ============================================================================
269
270/// MCP prompt definition
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct McpPrompt {
273    pub name: String,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub description: Option<String>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub arguments: Option<Vec<PromptArgument>>,
278}
279
280/// Prompt argument
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct PromptArgument {
283    pub name: String,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub description: Option<String>,
286    #[serde(default)]
287    pub required: bool,
288}
289
290/// List prompts result
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ListPromptsResult {
293    pub prompts: Vec<McpPrompt>,
294}
295
296// ============================================================================
297// MCP Notifications
298// ============================================================================
299
300/// MCP notification types
301#[derive(Debug, Clone)]
302pub enum McpNotification {
303    ToolsListChanged,
304    ResourcesListChanged,
305    PromptsListChanged,
306    Progress {
307        progress_token: String,
308        progress: f64,
309        total: Option<f64>,
310    },
311    Log {
312        level: String,
313        logger: Option<String>,
314        data: serde_json::Value,
315    },
316    Unknown {
317        method: String,
318        params: Option<serde_json::Value>,
319    },
320}
321
322impl McpNotification {
323    pub fn from_json_rpc(notification: &JsonRpcNotification) -> Self {
324        match notification.method.as_str() {
325            "notifications/tools/list_changed" => McpNotification::ToolsListChanged,
326            "notifications/resources/list_changed" => McpNotification::ResourcesListChanged,
327            "notifications/prompts/list_changed" => McpNotification::PromptsListChanged,
328            "notifications/progress" => {
329                if let Some(params) = &notification.params {
330                    let progress_token = params
331                        .get("progressToken")
332                        .and_then(|v| v.as_str())
333                        .unwrap_or("")
334                        .to_string();
335                    let progress = params
336                        .get("progress")
337                        .and_then(|v| v.as_f64())
338                        .unwrap_or(0.0);
339                    let total = params.get("total").and_then(|v| v.as_f64());
340                    McpNotification::Progress {
341                        progress_token,
342                        progress,
343                        total,
344                    }
345                } else {
346                    McpNotification::Unknown {
347                        method: notification.method.clone(),
348                        params: notification.params.clone(),
349                    }
350                }
351            }
352            "notifications/message" => {
353                if let Some(params) = &notification.params {
354                    let level = params
355                        .get("level")
356                        .and_then(|v| v.as_str())
357                        .unwrap_or("info")
358                        .to_string();
359                    let logger = params
360                        .get("logger")
361                        .and_then(|v| v.as_str())
362                        .map(|s| s.to_string());
363                    let data = params
364                        .get("data")
365                        .cloned()
366                        .unwrap_or(serde_json::Value::Null);
367                    McpNotification::Log {
368                        level,
369                        logger,
370                        data,
371                    }
372                } else {
373                    McpNotification::Unknown {
374                        method: notification.method.clone(),
375                        params: notification.params.clone(),
376                    }
377                }
378            }
379            _ => McpNotification::Unknown {
380                method: notification.method.clone(),
381                params: notification.params.clone(),
382            },
383        }
384    }
385}
386
387// ============================================================================
388// Configuration Types
389// ============================================================================
390
391/// MCP server configuration
392#[derive(Debug, Clone, Serialize)]
393pub struct McpServerConfig {
394    /// Server name (used for tool prefix)
395    pub name: String,
396    /// Transport configuration
397    pub transport: McpTransportConfig,
398    /// Whether enabled
399    #[serde(default = "default_true")]
400    pub enabled: bool,
401    /// Environment variables
402    #[serde(default)]
403    pub env: HashMap<String, String>,
404    /// OAuth configuration (optional)
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub oauth: Option<OAuthConfig>,
407    /// Per-tool execution timeout in seconds (default: 60)
408    #[serde(default = "default_tool_timeout")]
409    pub tool_timeout_secs: u64,
410}
411
412impl<'de> Deserialize<'de> for McpServerConfig {
413    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
414        use serde::de::Error;
415        use serde_json::Value;
416
417        let mut map = serde_json::Map::deserialize(deserializer)?;
418
419        // Build transport from flat ACL fields (transport = "stdio", command = "...", args = [...])
420        // or from a nested transport object ({ type = "stdio", command = "..." })
421        let transport = if let Some(t) = map.remove("transport") {
422            match &t {
423                Value::String(kind) => {
424                    // Flat ACL-like format: transport = "stdio", command = "...", args = [...]
425                    match kind.as_str() {
426                        "stdio" => {
427                            let command = map
428                                .remove("command")
429                                .and_then(|v| v.as_str().map(String::from))
430                                .ok_or_else(|| D::Error::missing_field("command"))?;
431                            let args = map
432                                .remove("args")
433                                .and_then(|v| serde_json::from_value(v).ok())
434                                .unwrap_or_default();
435                            McpTransportConfig::Stdio { command, args }
436                        }
437                        "http" => {
438                            let url = map
439                                .remove("url")
440                                .and_then(|v| v.as_str().map(String::from))
441                                .ok_or_else(|| D::Error::missing_field("url"))?;
442                            let headers = map
443                                .remove("headers")
444                                .and_then(|v| serde_json::from_value(v).ok())
445                                .unwrap_or_default();
446                            McpTransportConfig::Http { url, headers }
447                        }
448                        "streamable-http" | "streamable_http" => {
449                            let url = map
450                                .remove("url")
451                                .and_then(|v| v.as_str().map(String::from))
452                                .ok_or_else(|| D::Error::missing_field("url"))?;
453                            let headers = map
454                                .remove("headers")
455                                .and_then(|v| serde_json::from_value(v).ok())
456                                .unwrap_or_default();
457                            McpTransportConfig::StreamableHttp { url, headers }
458                        }
459                        other => {
460                            return Err(D::Error::unknown_variant(
461                                other,
462                                &["stdio", "http", "streamable-http"],
463                            ));
464                        }
465                    }
466                }
467                // Nested object format: transport { type = "stdio", command = "..." }
468                Value::Object(_) => serde_json::from_value(t).map_err(D::Error::custom)?,
469                _ => return Err(D::Error::custom("transport must be a string or object")),
470            }
471        } else {
472            return Err(D::Error::missing_field("transport"));
473        };
474
475        let name = map
476            .remove("name")
477            .and_then(|v| v.as_str().map(String::from))
478            .ok_or_else(|| D::Error::missing_field("name"))?;
479        let enabled = map
480            .remove("enabled")
481            .and_then(|v| v.as_bool())
482            .unwrap_or(true);
483        let env = map
484            .remove("env")
485            .and_then(|v| serde_json::from_value(v).ok())
486            .unwrap_or_default();
487        let oauth = map
488            .remove("oauth")
489            .and_then(|v| serde_json::from_value(v).ok());
490        let tool_timeout_secs = map
491            .remove("tool_timeout_secs")
492            .or_else(|| map.remove("toolTimeoutSecs"))
493            .and_then(|v| v.as_u64())
494            .unwrap_or(60);
495
496        Ok(McpServerConfig {
497            name,
498            transport,
499            enabled,
500            env,
501            oauth,
502            tool_timeout_secs,
503        })
504    }
505}
506
507#[allow(dead_code)] // used by serde default = "default_tool_timeout"
508fn default_tool_timeout() -> u64 {
509    60
510}
511
512#[allow(dead_code)]
513fn default_true() -> bool {
514    true
515}
516
517/// Transport configuration
518#[derive(Debug, Clone, Serialize, Deserialize)]
519#[serde(tag = "type", rename_all = "kebab-case")]
520pub enum McpTransportConfig {
521    /// Local process (stdio)
522    Stdio {
523        command: String,
524        #[serde(default)]
525        args: Vec<String>,
526    },
527    /// Remote HTTP + SSE (legacy, pre-2025-03-26)
528    Http {
529        url: String,
530        #[serde(default)]
531        headers: HashMap<String, String>,
532    },
533    /// Streamable HTTP (MCP 2025-03-26 spec)
534    ///
535    /// Single endpoint handles all communication.
536    /// POST with `Accept: application/json, text/event-stream`.
537    StreamableHttp {
538        url: String,
539        #[serde(default)]
540        headers: HashMap<String, String>,
541    },
542}
543
544/// OAuth configuration
545#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct OAuthConfig {
547    pub auth_url: String,
548    pub token_url: String,
549    pub client_id: String,
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub client_secret: Option<String>,
552    #[serde(default)]
553    pub scopes: Vec<String>,
554    pub redirect_uri: String,
555    /// Static access token — if set, skips the OAuth exchange flow.
556    /// Useful for long-lived tokens or service accounts.
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub access_token: Option<String>,
559}
560
561#[cfg(test)]
562#[path = "protocol/tests.rs"]
563mod tests;