Skip to main content

crates_docs/server/
handler.rs

1//! MCP handler implementation
2
3use crate::server::CratesDocsServer;
4use crate::tools::ToolRegistry;
5use async_trait::async_trait;
6use rust_mcp_sdk::{
7    mcp_server::{ServerHandler, ServerHandlerCore},
8    schema::{
9        CallToolError, CallToolRequestParams, CallToolResult, GetPromptRequestParams,
10        GetPromptResult, ListPromptsResult, ListResourcesResult, ListToolsResult,
11        NotificationFromClient, PaginatedRequestParams, ReadResourceRequestParams,
12        ReadResourceResult, RequestFromClient, ResultFromServer, RpcError,
13    },
14    McpServer,
15};
16use std::sync::Arc;
17use tracing::{info_span, Instrument};
18use uuid::Uuid;
19
20/// MCP server handler
21pub struct CratesDocsHandler {
22    server: Arc<CratesDocsServer>,
23}
24
25impl CratesDocsHandler {
26    /// Create a new handler
27    #[must_use]
28    pub fn new(server: Arc<CratesDocsServer>) -> Self {
29        Self { server }
30    }
31
32    /// Get the tool registry
33    fn tool_registry(&self) -> &ToolRegistry {
34        self.server.tool_registry()
35    }
36}
37
38#[async_trait]
39impl ServerHandler for CratesDocsHandler {
40    /// Handle list tools request
41    async fn handle_list_tools_request(
42        &self,
43        _request: Option<PaginatedRequestParams>,
44        _runtime: std::sync::Arc<dyn McpServer>,
45    ) -> std::result::Result<ListToolsResult, RpcError> {
46        let trace_id = Uuid::new_v4().to_string();
47        let span = info_span!(
48            "list_tools",
49            trace_id = %trace_id,
50        );
51
52        async {
53            tracing::debug!("Listing available tools");
54            let tools = self.tool_registry().get_tools();
55            tracing::debug!("Found {} tools", tools.len());
56
57            Ok(ListToolsResult {
58                tools,
59                meta: None,
60                next_cursor: None,
61            })
62        }
63        .instrument(span)
64        .await
65    }
66
67    /// Handle call tool request
68    async fn handle_call_tool_request(
69        &self,
70        params: CallToolRequestParams,
71        _runtime: std::sync::Arc<dyn McpServer>,
72    ) -> std::result::Result<CallToolResult, CallToolError> {
73        let trace_id = Uuid::new_v4().to_string();
74        let tool_name = params.name.clone();
75        let span = info_span!(
76            "call_tool",
77            trace_id = %trace_id,
78            tool = %tool_name,
79        );
80
81        async {
82            tracing::info!("Executing tool: {}", tool_name);
83            let start = std::time::Instant::now();
84
85            let result = self
86                .tool_registry()
87                .execute_tool(
88                    &tool_name,
89                    params
90                        .arguments
91                        .map_or_else(|| serde_json::Value::Null, serde_json::Value::Object),
92                )
93                .await;
94
95            let duration = start.elapsed();
96            match &result {
97                Ok(_) => {
98                    tracing::info!("Tool {} executed successfully in {:?}", tool_name, duration);
99                }
100                Err(e) => {
101                    tracing::error!(
102                        "Tool {} execution failed after {:?}: {:?}",
103                        tool_name,
104                        duration,
105                        e
106                    );
107                }
108            }
109
110            result
111        }
112        .instrument(span)
113        .await
114    }
115
116    /// Handle list resources request
117    async fn handle_list_resources_request(
118        &self,
119        _request: Option<PaginatedRequestParams>,
120        _runtime: std::sync::Arc<dyn McpServer>,
121    ) -> std::result::Result<ListResourcesResult, RpcError> {
122        // Resources are not currently provided
123        Ok(ListResourcesResult {
124            resources: vec![],
125            meta: None,
126            next_cursor: None,
127        })
128    }
129
130    /// Handle read resource request
131    async fn handle_read_resource_request(
132        &self,
133        _params: ReadResourceRequestParams,
134        _runtime: std::sync::Arc<dyn McpServer>,
135    ) -> std::result::Result<ReadResourceResult, RpcError> {
136        // Resources are not currently provided
137        Err(RpcError::invalid_request().with_message("Resource not found".to_string()))
138    }
139
140    /// Handle list prompts request
141    async fn handle_list_prompts_request(
142        &self,
143        _request: Option<PaginatedRequestParams>,
144        _runtime: std::sync::Arc<dyn McpServer>,
145    ) -> std::result::Result<ListPromptsResult, RpcError> {
146        // Prompts are not currently provided
147        Ok(ListPromptsResult {
148            prompts: vec![],
149            meta: None,
150            next_cursor: None,
151        })
152    }
153
154    /// Handle get prompt request
155    async fn handle_get_prompt_request(
156        &self,
157        _params: GetPromptRequestParams,
158        _runtime: std::sync::Arc<dyn McpServer>,
159    ) -> std::result::Result<GetPromptResult, RpcError> {
160        // Prompts are not currently provided
161        Err(RpcError::invalid_request().with_message("Prompt not found".to_string()))
162    }
163}
164
165/// Core handler implementation (provides more control)
166pub struct CratesDocsHandlerCore {
167    server: Arc<CratesDocsServer>,
168}
169
170impl CratesDocsHandlerCore {
171    /// Create a new core handler
172    #[must_use]
173    pub fn new(server: Arc<CratesDocsServer>) -> Self {
174        Self { server }
175    }
176
177    async fn execute_tool_request(&self, params: CallToolRequestParams) -> ResultFromServer {
178        let trace_id = Uuid::new_v4().to_string();
179        let tool_name = params.name.clone();
180        let span = info_span!(
181            "execute_tool_core",
182            trace_id = %trace_id,
183            tool = %tool_name,
184        );
185
186        async {
187            tracing::info!("Executing tool request: {}", tool_name);
188            let start = std::time::Instant::now();
189
190            let result = self
191                .server
192                .tool_registry()
193                .execute_tool(
194                    &tool_name,
195                    params
196                        .arguments
197                        .map_or_else(|| serde_json::Value::Null, serde_json::Value::Object),
198                )
199                .await;
200
201            let duration = start.elapsed();
202            match &result {
203                Ok(_) => {
204                    tracing::info!("Tool {} executed successfully in {:?}", tool_name, duration);
205                }
206                Err(e) => {
207                    tracing::error!(
208                        "Tool {} execution failed after {:?}: {:?}",
209                        tool_name,
210                        duration,
211                        e
212                    );
213                }
214            }
215
216            result.unwrap_or_else(CallToolResult::from).into()
217        }
218        .instrument(span)
219        .await
220    }
221}
222
223#[async_trait]
224impl ServerHandlerCore for CratesDocsHandlerCore {
225    /// Handle request
226    async fn handle_request(
227        &self,
228        request: RequestFromClient,
229        _runtime: std::sync::Arc<dyn McpServer>,
230    ) -> std::result::Result<ResultFromServer, RpcError> {
231        match request {
232            RequestFromClient::ListToolsRequest(_params) => {
233                let tools = self.server.tool_registry().get_tools();
234                Ok(ListToolsResult {
235                    tools,
236                    meta: None,
237                    next_cursor: None,
238                }
239                .into())
240            }
241            RequestFromClient::CallToolRequest(params) => {
242                Ok(self.execute_tool_request(params).await)
243            }
244            RequestFromClient::ListResourcesRequest(_params) => Ok(ListResourcesResult {
245                resources: vec![],
246                meta: None,
247                next_cursor: None,
248            }
249            .into()),
250            RequestFromClient::ReadResourceRequest(_params) => {
251                Err(RpcError::invalid_request().with_message("Resource not found".to_string()))
252            }
253            RequestFromClient::ListPromptsRequest(_params) => Ok(ListPromptsResult {
254                prompts: vec![],
255                meta: None,
256                next_cursor: None,
257            }
258            .into()),
259            RequestFromClient::GetPromptRequest(_params) => {
260                Err(RpcError::invalid_request().with_message("Prompt not found".to_string()))
261            }
262            RequestFromClient::InitializeRequest(_params) => {
263                // Use default initialization handling
264                Err(RpcError::method_not_found()
265                    .with_message("Initialize request should be handled by runtime".to_string()))
266            }
267            _ => {
268                // Other requests use default handling
269                Err(RpcError::method_not_found()
270                    .with_message("Unimplemented request type".to_string()))
271            }
272        }
273    }
274
275    /// Handle notification
276    async fn handle_notification(
277        &self,
278        _notification: NotificationFromClient,
279        _runtime: std::sync::Arc<dyn McpServer>,
280    ) -> std::result::Result<(), RpcError> {
281        // Notifications are not currently handled
282        Ok(())
283    }
284
285    /// Handle error
286    async fn handle_error(
287        &self,
288        _error: &RpcError,
289        _runtime: std::sync::Arc<dyn McpServer>,
290    ) -> std::result::Result<(), RpcError> {
291        // Log error but don't interrupt
292        tracing::error!("MCP error: {:?}", _error);
293        Ok(())
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::CratesDocsHandlerCore;
300    use crate::server::CratesDocsServer;
301    use rust_mcp_sdk::schema::{CallToolRequestParams, CallToolResult, ContentBlock};
302    use std::sync::Arc;
303
304    #[tokio::test]
305    async fn test_execute_tool_request_preserves_tool_errors() {
306        let server = Arc::new(CratesDocsServer::new(crate::AppConfig::default()).unwrap());
307        let handler = CratesDocsHandlerCore::new(server);
308        let result = handler
309            .execute_tool_request(CallToolRequestParams {
310                arguments: Some(serde_json::Map::from_iter([(
311                    "verbose".to_string(),
312                    serde_json::Value::String("bad".to_string()),
313                )])),
314                meta: None,
315                name: "health_check".to_string(),
316                task: None,
317            })
318            .await;
319
320        let result = CallToolResult::try_from(result).unwrap();
321        assert_eq!(result.is_error, Some(true));
322
323        let Some(ContentBlock::TextContent(text)) = result.content.first() else {
324            panic!("expected first content block to be text");
325        };
326
327        assert!(text.text.contains("health_check"));
328        assert!(text.text.contains("Parameter parsing failed"));
329        assert!(!text.text.contains("Unknown tool"));
330    }
331}