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 title: Option<String>,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub description: Option<String>,
178    pub input_schema: serde_json::Value,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub output_schema: Option<serde_json::Value>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub annotations: Option<McpToolAnnotations>,
183    #[serde(default, skip_serializing_if = "Vec::is_empty")]
184    pub icons: Vec<serde_json::Value>,
185    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
186    pub meta: Option<serde_json::Value>,
187}
188
189/// Standard MCP tool behavior hints.
190///
191/// These fields remain untrusted hints. Hosts may use them to request more
192/// confirmation, but never to weaken an existing permission policy.
193#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct McpToolAnnotations {
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub title: Option<String>,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub read_only_hint: Option<bool>,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub destructive_hint: Option<bool>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub idempotent_hint: Option<bool>,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub open_world_hint: Option<bool>,
206    #[serde(flatten)]
207    pub additional: HashMap<String, serde_json::Value>,
208}
209
210/// List tools result
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct ListToolsResult {
213    pub tools: Vec<McpTool>,
214}
215
216/// Call tool params
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct CallToolParams {
219    pub name: String,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub arguments: Option<serde_json::Value>,
222}
223
224/// Tool content types
225#[derive(Debug, Clone, Serialize, Deserialize)]
226#[serde(tag = "type", rename_all = "lowercase")]
227pub enum ToolContent {
228    Text {
229        text: String,
230    },
231    Image {
232        data: String,
233        #[serde(rename = "mimeType")]
234        mime_type: String,
235    },
236    Resource {
237        resource: ResourceContent,
238    },
239}
240
241/// Resource content
242#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(rename_all = "camelCase")]
244pub struct ResourceContent {
245    pub uri: String,
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub mime_type: Option<String>,
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub text: Option<String>,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub blob: Option<String>,
252}
253
254/// Call tool result
255#[derive(Debug, Clone, Default, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct CallToolResult {
258    #[serde(default)]
259    pub content: Vec<ToolContent>,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub structured_content: Option<serde_json::Value>,
262    #[serde(default)]
263    pub is_error: bool,
264    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
265    pub meta: Option<serde_json::Value>,
266}
267
268// ============================================================================
269// MCP Resources
270// ============================================================================
271
272/// MCP resource definition
273#[derive(Debug, Clone, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct McpResource {
276    pub uri: String,
277    pub name: String,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub description: Option<String>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub mime_type: Option<String>,
282}
283
284/// List resources result
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct ListResourcesResult {
287    pub resources: Vec<McpResource>,
288}
289
290/// Read resource params
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ReadResourceParams {
293    pub uri: String,
294}
295
296/// Read resource result
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct ReadResourceResult {
299    pub contents: Vec<ResourceContent>,
300}
301
302// ============================================================================
303// MCP Prompts
304// ============================================================================
305
306/// MCP prompt definition
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct McpPrompt {
309    pub name: String,
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub description: Option<String>,
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub arguments: Option<Vec<PromptArgument>>,
314}
315
316/// Prompt argument
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct PromptArgument {
319    pub name: String,
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub description: Option<String>,
322    #[serde(default)]
323    pub required: bool,
324}
325
326/// List prompts result
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct ListPromptsResult {
329    pub prompts: Vec<McpPrompt>,
330}
331
332// ============================================================================
333// MCP Notifications
334// ============================================================================
335
336/// MCP notification types
337#[derive(Debug, Clone)]
338pub enum McpNotification {
339    ToolsListChanged,
340    ResourcesListChanged,
341    PromptsListChanged,
342    Progress {
343        progress_token: String,
344        progress: f64,
345        total: Option<f64>,
346    },
347    Log {
348        level: String,
349        logger: Option<String>,
350        data: serde_json::Value,
351    },
352    Unknown {
353        method: String,
354        params: Option<serde_json::Value>,
355    },
356}
357
358impl McpNotification {
359    pub fn from_json_rpc(notification: &JsonRpcNotification) -> Self {
360        match notification.method.as_str() {
361            "notifications/tools/list_changed" => McpNotification::ToolsListChanged,
362            "notifications/resources/list_changed" => McpNotification::ResourcesListChanged,
363            "notifications/prompts/list_changed" => McpNotification::PromptsListChanged,
364            "notifications/progress" => {
365                if let Some(params) = &notification.params {
366                    let progress_token = params
367                        .get("progressToken")
368                        .and_then(|v| v.as_str())
369                        .unwrap_or("")
370                        .to_string();
371                    let progress = params
372                        .get("progress")
373                        .and_then(|v| v.as_f64())
374                        .unwrap_or(0.0);
375                    let total = params.get("total").and_then(|v| v.as_f64());
376                    McpNotification::Progress {
377                        progress_token,
378                        progress,
379                        total,
380                    }
381                } else {
382                    McpNotification::Unknown {
383                        method: notification.method.clone(),
384                        params: notification.params.clone(),
385                    }
386                }
387            }
388            "notifications/message" => {
389                if let Some(params) = &notification.params {
390                    let level = params
391                        .get("level")
392                        .and_then(|v| v.as_str())
393                        .unwrap_or("info")
394                        .to_string();
395                    let logger = params
396                        .get("logger")
397                        .and_then(|v| v.as_str())
398                        .map(|s| s.to_string());
399                    let data = params
400                        .get("data")
401                        .cloned()
402                        .unwrap_or(serde_json::Value::Null);
403                    McpNotification::Log {
404                        level,
405                        logger,
406                        data,
407                    }
408                } else {
409                    McpNotification::Unknown {
410                        method: notification.method.clone(),
411                        params: notification.params.clone(),
412                    }
413                }
414            }
415            _ => McpNotification::Unknown {
416                method: notification.method.clone(),
417                params: notification.params.clone(),
418            },
419        }
420    }
421}
422
423// ============================================================================
424// Configuration Types
425// ============================================================================
426
427/// MCP server configuration
428#[derive(Debug, Clone, Serialize)]
429pub struct McpServerConfig {
430    /// Server name (used for tool prefix)
431    pub name: String,
432    /// Transport configuration
433    pub transport: McpTransportConfig,
434    /// Whether enabled
435    #[serde(default = "default_true")]
436    pub enabled: bool,
437    /// Environment variables
438    #[serde(default)]
439    pub env: HashMap<String, String>,
440    /// OAuth configuration (optional)
441    #[serde(skip_serializing_if = "Option::is_none")]
442    pub oauth: Option<OAuthConfig>,
443    /// Per-tool execution timeout in seconds (default: 60)
444    #[serde(default = "default_tool_timeout")]
445    pub tool_timeout_secs: u64,
446}
447
448impl<'de> Deserialize<'de> for McpServerConfig {
449    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
450        use serde::de::Error;
451        use serde_json::Value;
452
453        let mut map = serde_json::Map::deserialize(deserializer)?;
454
455        // Build transport from flat ACL fields (transport = "stdio", command = "...", args = [...])
456        // or from a nested transport object ({ type = "stdio", command = "..." })
457        let transport = if let Some(t) = map.remove("transport") {
458            match &t {
459                Value::String(kind) => {
460                    // Flat ACL-like format: transport = "stdio", command = "...", args = [...]
461                    match kind.as_str() {
462                        "stdio" => {
463                            let command = map
464                                .remove("command")
465                                .and_then(|v| v.as_str().map(String::from))
466                                .ok_or_else(|| D::Error::missing_field("command"))?;
467                            let args = map
468                                .remove("args")
469                                .and_then(|v| serde_json::from_value(v).ok())
470                                .unwrap_or_default();
471                            McpTransportConfig::Stdio { command, args }
472                        }
473                        "http" => {
474                            let url = map
475                                .remove("url")
476                                .and_then(|v| v.as_str().map(String::from))
477                                .ok_or_else(|| D::Error::missing_field("url"))?;
478                            let headers = map
479                                .remove("headers")
480                                .and_then(|v| serde_json::from_value(v).ok())
481                                .unwrap_or_default();
482                            McpTransportConfig::Http { url, headers }
483                        }
484                        "streamable-http" | "streamable_http" => {
485                            let url = map
486                                .remove("url")
487                                .and_then(|v| v.as_str().map(String::from))
488                                .ok_or_else(|| D::Error::missing_field("url"))?;
489                            let headers = map
490                                .remove("headers")
491                                .and_then(|v| serde_json::from_value(v).ok())
492                                .unwrap_or_default();
493                            McpTransportConfig::StreamableHttp { url, headers }
494                        }
495                        other => {
496                            return Err(D::Error::unknown_variant(
497                                other,
498                                &["stdio", "http", "streamable-http"],
499                            ));
500                        }
501                    }
502                }
503                // Nested object format: transport { type = "stdio", command = "..." }
504                Value::Object(_) => serde_json::from_value(t).map_err(D::Error::custom)?,
505                _ => return Err(D::Error::custom("transport must be a string or object")),
506            }
507        } else {
508            return Err(D::Error::missing_field("transport"));
509        };
510
511        let name = map
512            .remove("name")
513            .and_then(|v| v.as_str().map(String::from))
514            .ok_or_else(|| D::Error::missing_field("name"))?;
515        let enabled = map
516            .remove("enabled")
517            .and_then(|v| v.as_bool())
518            .unwrap_or(true);
519        let env = map
520            .remove("env")
521            .and_then(|v| serde_json::from_value(v).ok())
522            .unwrap_or_default();
523        let oauth = map
524            .remove("oauth")
525            .and_then(|v| serde_json::from_value(v).ok());
526        let tool_timeout_secs = map
527            .remove("tool_timeout_secs")
528            .or_else(|| map.remove("toolTimeoutSecs"))
529            .and_then(|v| v.as_u64())
530            .unwrap_or(60);
531
532        Ok(McpServerConfig {
533            name,
534            transport,
535            enabled,
536            env,
537            oauth,
538            tool_timeout_secs,
539        })
540    }
541}
542
543#[allow(dead_code)] // used by serde default = "default_tool_timeout"
544fn default_tool_timeout() -> u64 {
545    60
546}
547
548#[allow(dead_code)]
549fn default_true() -> bool {
550    true
551}
552
553/// Transport configuration
554#[derive(Debug, Clone, Serialize, Deserialize)]
555#[serde(tag = "type", rename_all = "kebab-case")]
556pub enum McpTransportConfig {
557    /// Local process (stdio)
558    Stdio {
559        command: String,
560        #[serde(default)]
561        args: Vec<String>,
562    },
563    /// Remote HTTP + SSE (legacy, pre-2025-03-26)
564    Http {
565        url: String,
566        #[serde(default)]
567        headers: HashMap<String, String>,
568    },
569    /// Streamable HTTP (MCP 2025-03-26 spec)
570    ///
571    /// Single endpoint handles all communication.
572    /// POST with `Accept: application/json, text/event-stream`.
573    StreamableHttp {
574        url: String,
575        #[serde(default)]
576        headers: HashMap<String, String>,
577    },
578}
579
580/// OAuth configuration
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct OAuthConfig {
583    pub auth_url: String,
584    pub token_url: String,
585    pub client_id: String,
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub client_secret: Option<String>,
588    #[serde(default)]
589    pub scopes: Vec<String>,
590    pub redirect_uri: String,
591    /// Static access token — if set, skips the OAuth exchange flow.
592    /// Useful for long-lived tokens or service accounts.
593    #[serde(skip_serializing_if = "Option::is_none")]
594    pub access_token: Option<String>,
595}
596
597#[cfg(test)]
598#[path = "protocol/tests.rs"]
599mod tests;