Skip to main content

do_memory_mcp/protocol/
handlers.rs

1//! MCP Protocol handlers
2//!
3//! This module contains core MCP protocol handlers:
4//! - handle_initialize: Initialize request handler
5//! - handle_list_tools: List available tools
6//! - handle_shutdown: Shutdown the server
7
8use super::types::*;
9use crate::jsonrpc::{JsonRpcError, JsonRpcRequest, JsonRpcResponse};
10use serde_json::json;
11use tracing::{error, info};
12
13/// Handle initialize request
14pub async fn handle_initialize(
15    request: JsonRpcRequest,
16    oauth_config: &OAuthConfig,
17) -> Option<JsonRpcResponse> {
18    // Notifications must not produce a response
19    request.id.as_ref()?;
20
21    // Extract client's requested protocol version
22    let requested_version = request
23        .params
24        .as_ref()
25        .and_then(|params| params.get("protocolVersion").and_then(|v| v.as_str()));
26
27    // Negotiate protocol version
28    let protocol_version = match requested_version {
29        Some(version) => {
30            if SUPPORTED_VERSIONS.contains(&version) {
31                version.to_string()
32            } else {
33                // Client requested unsupported version, return the latest we support
34                info!(
35                    "Client requested unsupported protocol version '{}', using latest '{}'",
36                    version, SUPPORTED_VERSIONS[0]
37                );
38                SUPPORTED_VERSIONS[0].to_string()
39            }
40        }
41        None => {
42            // No version requested, use latest
43            SUPPORTED_VERSIONS[0].to_string()
44        }
45    };
46
47    info!("Negotiated protocol version: {}", protocol_version);
48
49    // Build capabilities object
50    let mut capabilities = json!({
51        "tools": {
52            "listChanged": false
53        },
54        "completions": {},
55        "elicitation": {},
56        "tasks": {
57            "list": {},
58            "create": {},
59            "update": {}
60        }
61    });
62
63    // Add OAuth 2.1 authorization capability if enabled
64    if oauth_config.enabled {
65        capabilities["authorization"] = json!({
66            "enabled": true,
67            "issuer": oauth_config.issuer.clone().unwrap_or_default(),
68            "audience": oauth_config.audience.clone().unwrap_or_default(),
69            "scopes": oauth_config.scopes
70        });
71    }
72
73    let result = InitializeResult {
74        protocol_version,
75        capabilities,
76        server_info: json!({
77            "name": "do-memory-mcp-server",
78            "version": env!("CARGO_PKG_VERSION")
79        }),
80    };
81
82    match serde_json::to_value(result) {
83        Ok(value) => Some(JsonRpcResponse {
84            jsonrpc: "2.0".to_string(),
85            id: request.id,
86            result: Some(value),
87            error: None,
88        }),
89        Err(e) => {
90            error!("Failed to serialize initialize response: {}", e);
91            Some(JsonRpcResponse {
92                jsonrpc: "2.0".to_string(),
93                id: request.id,
94                result: None,
95                error: Some(JsonRpcError {
96                    code: -32603,
97                    message: "Internal error".to_string(),
98                    data: Some(json!({"details": format!("Response serialization failed: {}", e)})),
99                }),
100            })
101        }
102    }
103}
104
105/// Handle tools/list request
106pub async fn handle_list_tools(
107    request: JsonRpcRequest,
108    tools: Vec<McpTool>,
109) -> Option<JsonRpcResponse> {
110    // Notifications must not produce a response
111    request.id.as_ref()?;
112    info!("Handling tools/list request");
113
114    let result = ListToolsResult { tools };
115
116    match serde_json::to_value(result) {
117        Ok(value) => Some(JsonRpcResponse {
118            jsonrpc: "2.0".to_string(),
119            id: request.id,
120            result: Some(value),
121            error: None,
122        }),
123        Err(e) => {
124            error!("Failed to serialize list_tools response: {}", e);
125            Some(JsonRpcResponse {
126                jsonrpc: "2.0".to_string(),
127                id: request.id,
128                result: None,
129                error: Some(JsonRpcError {
130                    code: -32603,
131                    message: "Internal error".to_string(),
132                    data: Some(json!({"details": format!("Response serialization failed: {}", e)})),
133                }),
134            })
135        }
136    }
137}
138
139/// Handle shutdown request
140pub async fn handle_shutdown(request: JsonRpcRequest) -> Option<JsonRpcResponse> {
141    // Notifications must not produce a response
142    request.id.as_ref()?;
143    info!("Handling shutdown request");
144
145    Some(JsonRpcResponse {
146        jsonrpc: "2.0".to_string(),
147        id: request.id,
148        result: Some(json!(null)),
149        error: None,
150    })
151}
152
153/// Handle tools/list request with lazy loading support (ADR-024)
154///
155/// Supports lazy loading via the `lazy` parameter:
156/// - `lazy=true`: Returns lightweight tool stubs (90-96% token reduction)
157/// - `lazy=false` or `lazy` not specified: Returns full tool schemas (backward compatible)
158///
159/// # Arguments
160///
161/// * `request` - The JSON-RPC request
162/// * `tools` - The tools to list (full Tool objects)
163///
164/// # Returns
165///
166/// JSON-RPC response with either `ListToolStubsResult` (lazy=true) or `ListToolsResult` (lazy=false)
167pub fn handle_list_tools_with_lazy(
168    request: JsonRpcRequest,
169    tools: Vec<crate::types::Tool>,
170) -> Option<JsonRpcResponse> {
171    // Notifications must not produce a response
172    request.id.as_ref()?;
173    info!("Handling tools/list request");
174
175    // Check if lazy loading is enabled (default: false for compatibility)
176    let lazy = request
177        .params
178        .as_ref()
179        .and_then(|p| p.get("lazy"))
180        .and_then(|v| v.as_bool())
181        .unwrap_or(false);
182
183    if lazy {
184        // Return lightweight stubs (90-96% token reduction)
185        let tool_stubs: Vec<ToolStub> = tools
186            .into_iter()
187            .map(|tool| ToolStub {
188                name: tool.name,
189                title: None,
190                description: tool.description,
191            })
192            .collect();
193
194        let result = ListToolStubsResult { tools: tool_stubs };
195
196        match serde_json::to_value(result) {
197            Ok(value) => Some(JsonRpcResponse {
198                jsonrpc: "2.0".to_string(),
199                id: request.id,
200                result: Some(value),
201                error: None,
202            }),
203            Err(e) => {
204                error!("Failed to serialize list_tools response: {}", e);
205                Some(JsonRpcResponse {
206                    jsonrpc: "2.0".to_string(),
207                    id: request.id,
208                    result: None,
209                    error: Some(JsonRpcError {
210                        code: -32603,
211                        message: "Internal error".to_string(),
212                        data: Some(
213                            json!({"details": format!("Response serialization failed: {}", e)}),
214                        ),
215                    }),
216                })
217            }
218        }
219    } else {
220        // Return full schemas (backward compatible)
221        let mcp_tools: Vec<McpTool> = tools
222            .into_iter()
223            .map(|tool| McpTool {
224                name: tool.name,
225                title: None,
226                description: tool.description,
227                input_schema: tool.input_schema,
228            })
229            .collect();
230
231        let result = ListToolsResult { tools: mcp_tools };
232
233        match serde_json::to_value(result) {
234            Ok(value) => Some(JsonRpcResponse {
235                jsonrpc: "2.0".to_string(),
236                id: request.id,
237                result: Some(value),
238                error: None,
239            }),
240            Err(e) => {
241                error!("Failed to serialize list_tools response: {}", e);
242                Some(JsonRpcResponse {
243                    jsonrpc: "2.0".to_string(),
244                    id: request.id,
245                    result: None,
246                    error: Some(JsonRpcError {
247                        code: -32603,
248                        message: "Internal error".to_string(),
249                        data: Some(
250                            json!({"details": format!("Response serialization failed: {}", e)}),
251                        ),
252                    }),
253                })
254            }
255        }
256    }
257}
258
259/// Handle tools/describe request (ADR-024)
260///
261/// Returns full schema for a single tool (on-demand loading after lazy list).
262///
263/// # Arguments
264///
265/// * `request` - The JSON-RPC request with `name` parameter
266/// * `get_tool` - Function to get a tool by name (returns `Option<Tool>`)
267///
268/// # Returns
269///
270/// JSON-RPC response with `DescribeToolResult` or error if tool not found
271pub fn handle_describe_tool<F>(request: JsonRpcRequest, get_tool: F) -> Option<JsonRpcResponse>
272where
273    F: FnOnce(&str) -> Option<crate::types::Tool>,
274{
275    request.id.as_ref()?;
276    info!("Handling tools/describe request");
277
278    // Extract tool name from params
279    let tool_name = request
280        .params
281        .as_ref()
282        .and_then(|p| p.get("name"))
283        .and_then(|v| v.as_str());
284
285    let tool_name = match tool_name {
286        Some(name) => name,
287        None => {
288            return Some(JsonRpcResponse {
289                jsonrpc: "2.0".to_string(),
290                id: request.id,
291                result: None,
292                error: Some(JsonRpcError {
293                    code: -32602,
294                    message: "Invalid params".to_string(),
295                    data: Some(json!({"details": "Missing required parameter: name"})),
296                }),
297            });
298        }
299    };
300
301    let tool = get_tool(tool_name);
302
303    match tool {
304        Some(tool) => {
305            let mcp_tool = McpTool {
306                name: tool.name,
307                title: None,
308                description: tool.description,
309                input_schema: tool.input_schema,
310            };
311
312            let result = DescribeToolResult { tool: mcp_tool };
313
314            match serde_json::to_value(result) {
315                Ok(value) => Some(JsonRpcResponse {
316                    jsonrpc: "2.0".to_string(),
317                    id: request.id,
318                    result: Some(value),
319                    error: None,
320                }),
321                Err(e) => {
322                    error!("Failed to serialize describe_tool response: {}", e);
323                    Some(JsonRpcResponse {
324                        jsonrpc: "2.0".to_string(),
325                        id: request.id,
326                        result: None,
327                        error: Some(JsonRpcError {
328                            code: -32603,
329                            message: "Internal error".to_string(),
330                            data: Some(
331                                json!({"details": format!("Response serialization failed: {}", e)}),
332                            ),
333                        }),
334                    })
335                }
336            }
337        }
338        None => {
339            info!("Tool not found: {}", tool_name);
340            Some(JsonRpcResponse {
341                jsonrpc: "2.0".to_string(),
342                id: request.id,
343                result: None,
344                error: Some(JsonRpcError {
345                    code: -32602,
346                    message: "Tool not found".to_string(),
347                    data: Some(json!({"tool_name": tool_name})),
348                }),
349            })
350        }
351    }
352}
353
354/// Handle tools/describe_batch request (ADR-024)
355///
356/// Returns full schemas for multiple tools (batch on-demand loading).
357///
358/// # Arguments
359///
360/// * `request` - The JSON-RPC request with `names` array parameter
361/// * `get_tool` - Function to get a tool by name (returns `Option<Tool>`)
362///
363/// # Returns
364///
365/// JSON-RPC response with `DescribeToolsResult` containing found tools
366pub fn handle_describe_tools<F>(request: JsonRpcRequest, get_tool: F) -> Option<JsonRpcResponse>
367where
368    F: Fn(&str) -> Option<crate::types::Tool>,
369{
370    request.id.as_ref()?;
371    info!("Handling tools/describe_batch request");
372
373    // Extract tool names from params
374    let tool_names = request
375        .params
376        .as_ref()
377        .and_then(|p| p.get("names"))
378        .and_then(|v| v.as_array());
379
380    let tool_names = match tool_names {
381        Some(names) => names
382            .iter()
383            .filter_map(|v| v.as_str())
384            .map(String::from)
385            .collect::<Vec<_>>(),
386        None => {
387            return Some(JsonRpcResponse {
388                jsonrpc: "2.0".to_string(),
389                id: request.id,
390                result: None,
391                error: Some(JsonRpcError {
392                    code: -32602,
393                    message: "Invalid params".to_string(),
394                    data: Some(json!({"details": "Missing required parameter: names (array)"})),
395                }),
396            });
397        }
398    };
399
400    // Load tools by name
401    let mut mcp_tools = Vec::new();
402    for tool_name in &tool_names {
403        if let Some(tool) = get_tool(tool_name) {
404            mcp_tools.push(McpTool {
405                name: tool.name,
406                title: None,
407                description: tool.description,
408                input_schema: tool.input_schema,
409            });
410        }
411    }
412
413    let result = DescribeToolsResult { tools: mcp_tools };
414
415    match serde_json::to_value(result) {
416        Ok(value) => Some(JsonRpcResponse {
417            jsonrpc: "2.0".to_string(),
418            id: request.id,
419            result: Some(value),
420            error: None,
421        }),
422        Err(e) => {
423            error!("Failed to serialize describe_tools response: {}", e);
424            Some(JsonRpcResponse {
425                jsonrpc: "2.0".to_string(),
426                id: request.id,
427                result: None,
428                error: Some(JsonRpcError {
429                    code: -32603,
430                    message: "Internal error".to_string(),
431                    data: Some(json!({"details": format!("Response serialization failed: {}", e)})),
432                }),
433            })
434        }
435    }
436}