Skip to main content

engram/mcp/
protocol.rs

1//! MCP JSON-RPC protocol implementation
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::io::{BufRead, BufReader, Write};
6
7use crate::error::{EngramError, Result};
8
9/// MCP JSON-RPC request
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct McpRequest {
12    pub jsonrpc: String,
13    pub id: Option<Value>,
14    pub method: String,
15    #[serde(default)]
16    pub params: Value,
17}
18
19/// MCP JSON-RPC response
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct McpResponse {
22    pub jsonrpc: String,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub id: Option<Value>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub result: Option<Value>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub error: Option<McpError>,
29}
30
31/// MCP error object
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct McpError {
34    pub code: i64,
35    pub message: String,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub data: Option<Value>,
38}
39
40impl McpResponse {
41    /// Create a success response
42    pub fn success(id: Option<Value>, result: Value) -> Self {
43        Self {
44            jsonrpc: "2.0".to_string(),
45            id,
46            result: Some(result),
47            error: None,
48        }
49    }
50
51    /// Create an error response
52    pub fn error(id: Option<Value>, code: i64, message: String) -> Self {
53        Self {
54            jsonrpc: "2.0".to_string(),
55            id,
56            result: None,
57            error: Some(McpError {
58                code,
59                message,
60                data: None,
61            }),
62        }
63    }
64
65    /// Create error from EngramError
66    pub fn from_error(id: Option<Value>, err: EngramError) -> Self {
67        Self::error(id, err.code(), err.to_string())
68    }
69}
70
71/// MCP Server handling stdio communication
72pub struct McpServer<H>
73where
74    H: McpHandler,
75{
76    handler: H,
77}
78
79/// Trait for handling MCP requests
80pub trait McpHandler: Send + Sync {
81    fn handle_request(&self, request: McpRequest) -> McpResponse;
82}
83
84impl<T: McpHandler> McpHandler for std::sync::Arc<T> {
85    fn handle_request(&self, request: McpRequest) -> McpResponse {
86        (**self).handle_request(request)
87    }
88}
89
90impl<H: McpHandler> McpServer<H> {
91    /// Create a new MCP server
92    pub fn new(handler: H) -> Self {
93        Self { handler }
94    }
95
96    /// Run the server, reading from stdin and writing to stdout
97    pub fn run(&self) -> Result<()> {
98        let stdin = std::io::stdin();
99        let stdout = std::io::stdout();
100        let mut reader = BufReader::new(stdin.lock());
101        let mut writer = stdout.lock();
102
103        let mut line = String::new();
104
105        loop {
106            line.clear();
107            match reader.read_line(&mut line) {
108                Ok(0) => break, // EOF
109                Ok(_) => {
110                    let trimmed = line.trim();
111                    if trimmed.is_empty() {
112                        continue;
113                    }
114
115                    match serde_json::from_str::<McpRequest>(trimmed) {
116                        Ok(request) => {
117                            // Per JSON-RPC 2.0: notifications have no id and MUST NOT
118                            // produce a response. Process for side effects only.
119                            let is_notification = request.id.is_none();
120                            let response = self.handler.handle_request(request);
121                            if is_notification {
122                                continue;
123                            }
124                            let response_json = serde_json::to_string(&response)?;
125                            writeln!(writer, "{}", response_json)?;
126                            writer.flush()?;
127                        }
128                        Err(e) => {
129                            let response =
130                                McpResponse::error(None, -32700, format!("Parse error: {}", e));
131                            let response_json = serde_json::to_string(&response)?;
132                            writeln!(writer, "{}", response_json)?;
133                            writer.flush()?;
134                        }
135                    }
136                }
137                Err(e) => {
138                    tracing::error!("Error reading stdin: {}", e);
139                    break;
140                }
141            }
142        }
143
144        Ok(())
145    }
146}
147
148/// Standard MCP methods
149pub mod methods {
150    pub const INITIALIZE: &str = "initialize";
151    pub const INITIALIZED: &str = "notifications/initialized";
152    pub const LIST_TOOLS: &str = "tools/list";
153    pub const CALL_TOOL: &str = "tools/call";
154    pub const LIST_RESOURCES: &str = "resources/list";
155    pub const READ_RESOURCE: &str = "resources/read";
156    pub const LIST_PROMPTS: &str = "prompts/list";
157    pub const GET_PROMPT: &str = "prompts/get";
158}
159
160/// Current MCP protocol version supported by this server
161pub const MCP_PROTOCOL_VERSION: &str = "2025-11-25";
162/// Legacy MCP protocol version for backward compatibility
163pub const MCP_PROTOCOL_VERSION_LEGACY: &str = "2024-11-05";
164
165/// MCP tool annotations per MCP 2025-11-25 spec.
166///
167/// Hints about tool behavior — not security guarantees.
168#[derive(Debug, Clone, Serialize, Deserialize, Default)]
169pub struct ToolAnnotations {
170    /// If true, the tool does not modify any state.
171    #[serde(rename = "readOnlyHint", skip_serializing_if = "Option::is_none")]
172    pub read_only_hint: Option<bool>,
173
174    /// If true, the tool may perform destructive / irreversible operations.
175    #[serde(rename = "destructiveHint", skip_serializing_if = "Option::is_none")]
176    pub destructive_hint: Option<bool>,
177
178    /// If true, calling the tool multiple times with the same arguments produces
179    /// the same result as calling it once.
180    #[serde(rename = "idempotentHint", skip_serializing_if = "Option::is_none")]
181    pub idempotent_hint: Option<bool>,
182
183    /// If true, the tool may interact with an unbounded set of external entities
184    /// (e.g., the filesystem, network).
185    #[serde(rename = "openWorldHint", skip_serializing_if = "Option::is_none")]
186    pub open_world_hint: Option<bool>,
187}
188
189impl ToolAnnotations {
190    /// Read-only tool — does not modify state.
191    pub const fn read_only() -> Self {
192        Self {
193            read_only_hint: Some(true),
194            destructive_hint: None,
195            idempotent_hint: None,
196            open_world_hint: None,
197        }
198    }
199
200    /// Destructive tool — may perform irreversible operations.
201    pub const fn destructive() -> Self {
202        Self {
203            read_only_hint: None,
204            destructive_hint: Some(true),
205            idempotent_hint: None,
206            open_world_hint: None,
207        }
208    }
209
210    /// Idempotent tool — safe to call multiple times with the same args.
211    pub const fn idempotent() -> Self {
212        Self {
213            read_only_hint: None,
214            destructive_hint: None,
215            idempotent_hint: Some(true),
216            open_world_hint: None,
217        }
218    }
219
220    /// Plain mutating tool — creates or updates state but not destructive.
221    pub const fn mutating() -> Self {
222        Self {
223            read_only_hint: None,
224            destructive_hint: None,
225            idempotent_hint: None,
226            open_world_hint: None,
227        }
228    }
229}
230
231/// MCP tool definition
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct ToolDefinition {
234    pub name: String,
235    pub description: String,
236    #[serde(rename = "inputSchema")]
237    pub input_schema: Value,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub annotations: Option<ToolAnnotations>,
240}
241
242/// MCP initialize result
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct InitializeResult {
245    #[serde(rename = "protocolVersion")]
246    pub protocol_version: String,
247    pub capabilities: ServerCapabilities,
248    #[serde(rename = "serverInfo")]
249    pub server_info: ServerInfo,
250}
251
252/// Server capabilities
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct ServerCapabilities {
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub tools: Option<ToolsCapability>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub resources: Option<ResourceCapabilities>,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub prompts: Option<PromptCapabilities>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct ToolsCapability {
265    #[serde(rename = "listChanged")]
266    pub list_changed: bool,
267}
268
269/// Resource capabilities (MCP 2025-11-25)
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct ResourceCapabilities {
272    pub subscribe: bool,
273    #[serde(rename = "listChanged")]
274    pub list_changed: bool,
275}
276
277/// Prompt capabilities (MCP 2025-11-25)
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct PromptCapabilities {
280    #[serde(rename = "listChanged")]
281    pub list_changed: bool,
282}
283
284/// Server info
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct ServerInfo {
287    pub name: String,
288    pub version: String,
289}
290
291impl Default for InitializeResult {
292    fn default() -> Self {
293        Self {
294            protocol_version: MCP_PROTOCOL_VERSION.to_string(),
295            capabilities: ServerCapabilities {
296                tools: Some(ToolsCapability {
297                    list_changed: false,
298                }),
299                resources: Some(ResourceCapabilities {
300                    subscribe: false,
301                    list_changed: false,
302                }),
303                prompts: Some(PromptCapabilities {
304                    list_changed: false,
305                }),
306            },
307            server_info: ServerInfo {
308                name: "engram".to_string(),
309                version: env!("CARGO_PKG_VERSION").to_string(),
310            },
311        }
312    }
313}
314
315/// Tool call result
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct ToolCallResult {
318    pub content: Vec<ToolContent>,
319    #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
320    pub is_error: Option<bool>,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
324#[serde(tag = "type")]
325pub enum ToolContent {
326    #[serde(rename = "text")]
327    Text { text: String },
328    #[serde(rename = "image")]
329    Image { data: String, mime_type: String },
330    #[serde(rename = "resource")]
331    Resource { resource: ResourceContent },
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct ResourceContent {
336    pub uri: String,
337    pub text: Option<String>,
338    pub blob: Option<String>,
339    #[serde(rename = "mimeType")]
340    pub mime_type: Option<String>,
341}
342
343impl ToolCallResult {
344    /// Create a text result
345    pub fn text(text: impl Into<String>) -> Self {
346        Self {
347            content: vec![ToolContent::Text { text: text.into() }],
348            is_error: None,
349        }
350    }
351
352    /// Create a JSON result
353    pub fn json(value: &impl Serialize) -> Self {
354        let text = serde_json::to_string_pretty(value).unwrap_or_default();
355        Self::text(text)
356    }
357
358    /// Create an error result
359    pub fn error(message: impl Into<String>) -> Self {
360        Self {
361            content: vec![ToolContent::Text {
362                text: message.into(),
363            }],
364            is_error: Some(true),
365        }
366    }
367}
368
369/// Resource definition (MCP 2025-11-25)
370#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct ResourceDefinition {
372    pub uri: String,
373    pub name: String,
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub description: Option<String>,
376    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
377    pub mime_type: Option<String>,
378}
379
380/// Resource URI template definition (MCP 2025-11-25)
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct ResourceTemplate {
383    #[serde(rename = "uriTemplate")]
384    pub uri_template: String,
385    pub name: String,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub description: Option<String>,
388    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
389    pub mime_type: Option<String>,
390}
391
392/// Prompt definition (MCP 2025-11-25)
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct PromptDefinition {
395    pub name: String,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub description: Option<String>,
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub arguments: Option<Vec<PromptArgument>>,
400}
401
402/// Prompt argument definition (MCP 2025-11-25)
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct PromptArgument {
405    pub name: String,
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub description: Option<String>,
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub required: Option<bool>,
410}
411
412/// A message returned in a prompt response (MCP 2025-11-25)
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct PromptMessage {
415    /// "user" or "assistant"
416    pub role: String,
417    pub content: PromptContent,
418}
419
420/// Text content within a prompt message (MCP 2025-11-25)
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct PromptContent {
423    #[serde(rename = "type")]
424    pub content_type: String,
425    pub text: String,
426}