Skip to main content

crates_docs/server/handler/
standard.rs

1//! MCP server handler implementation
2
3use async_trait::async_trait;
4use rust_mcp_sdk::{
5    mcp_server::ServerHandler,
6    schema::{
7        CallToolError, CallToolRequestParams, CallToolResult, GetPromptRequestParams,
8        GetPromptResult, ListPromptsResult, ListResourcesResult, ListToolsResult,
9        PaginatedRequestParams, ReadResourceRequestParams, ReadResourceResult, RpcError,
10    },
11    McpServer,
12};
13use std::sync::Arc;
14use tracing::{info_span, Instrument};
15use uuid::Uuid;
16
17use super::config::HandlerConfig;
18use super::types::ToolExecutionResult;
19use crate::metrics::ServerMetrics;
20use crate::server::CratesDocsServer;
21use crate::tools::ToolRegistry;
22
23/// MCP server handler
24///
25/// Implements standard MCP protocol handler interface, handles client requests.
26///
27/// # Fields
28///
29/// - `server`: Server instance
30/// - `config`: Handler configuration
31/// - `metrics`: Optional metrics collector
32pub struct CratesDocsHandler {
33    server: Arc<CratesDocsServer>,
34    config: HandlerConfig,
35    metrics: Option<Arc<ServerMetrics>>,
36}
37
38impl CratesDocsHandler {
39    /// Create new handler
40    ///
41    /// # Arguments
42    ///
43    /// * `server` - Server instance
44    ///
45    /// # Example
46    ///
47    /// ```rust,no_run
48    /// use std::sync::Arc;
49    /// use crates_docs::server::{CratesDocsServer, CratesDocsHandler};
50    /// use crates_docs::AppConfig;
51    ///
52    /// let config = AppConfig::default();
53    /// let server = Arc::new(CratesDocsServer::new(config).unwrap());
54    /// let handler = CratesDocsHandler::new(server);
55    /// ```
56    #[must_use]
57    pub fn new(server: Arc<CratesDocsServer>) -> Self {
58        Self {
59            server,
60            config: HandlerConfig::default(),
61            metrics: None,
62        }
63    }
64
65    /// Create handler with configuration
66    #[must_use]
67    pub fn with_config(server: Arc<CratesDocsServer>, config: HandlerConfig) -> Self {
68        Self {
69            server,
70            config,
71            metrics: None,
72        }
73    }
74
75    /// Create handler with merged configuration
76    #[must_use]
77    pub fn with_merged_config(
78        server: Arc<CratesDocsServer>,
79        base_config: HandlerConfig,
80        override_config: Option<HandlerConfig>,
81    ) -> Self {
82        Self {
83            server,
84            config: base_config.merge(override_config),
85            metrics: None,
86        }
87    }
88
89    /// Set metrics
90    #[must_use]
91    pub fn with_metrics(self, metrics: Arc<ServerMetrics>) -> Self {
92        Self {
93            metrics: Some(metrics),
94            ..self
95        }
96    }
97
98    /// Get server reference
99    #[must_use]
100    pub fn server(&self) -> &Arc<CratesDocsServer> {
101        &self.server
102    }
103
104    /// Get tool registry
105    #[must_use]
106    pub fn tool_registry(&self) -> &ToolRegistry {
107        self.server.tool_registry()
108    }
109
110    /// Get configuration
111    #[must_use]
112    pub fn config(&self) -> &HandlerConfig {
113        &self.config
114    }
115
116    /// Get metrics (optional)
117    #[must_use]
118    pub fn metrics(&self) -> Option<&Arc<ServerMetrics>> {
119        self.metrics.as_ref()
120    }
121
122    /// Get all tools list
123    #[must_use]
124    pub fn list_tools(&self) -> ListToolsResult {
125        ListToolsResult {
126            tools: self.tool_registry().get_tools(),
127            meta: None,
128            next_cursor: None,
129        }
130    }
131
132    /// Get empty resources list
133    #[must_use]
134    pub fn list_resources(&self) -> ListResourcesResult {
135        ListResourcesResult {
136            resources: vec![],
137            meta: None,
138            next_cursor: None,
139        }
140    }
141
142    /// Get empty prompts list
143    #[must_use]
144    pub fn list_prompts(&self) -> ListPromptsResult {
145        ListPromptsResult {
146            prompts: vec![],
147            meta: None,
148            next_cursor: None,
149        }
150    }
151
152    /// Execute tool call (core logic)
153    ///
154    /// This method encapsulates the complete tool execution flow:
155    /// - tracing tracking
156    /// - timing statistics
157    /// - metrics recording (if enabled)
158    ///
159    /// # Returns
160    ///
161    /// Returns `ToolExecutionResult`, can be converted to different types to adapt to different traits
162    pub async fn execute_tool(&self, params: CallToolRequestParams) -> ToolExecutionResult {
163        let trace_id = Uuid::new_v4().to_string();
164        let tool_name = params.name.clone();
165        let span = info_span!(
166            "execute_tool",
167            trace_id = %trace_id,
168            tool = %tool_name,
169            verbose = self.config.verbose_logging,
170        );
171
172        async {
173            tracing::info!("Executing tool: {}", tool_name);
174            let start = std::time::Instant::now();
175
176            // An omitted `arguments` field is valid per the MCP spec
177            // (`CallToolRequestParams.arguments` is optional). Default to an
178            // empty object so tools whose parameters are all optional (e.g.
179            // `health_check`) still deserialize and run with their defaults,
180            // and tools with required fields produce a clear
181            // "missing field ..." error instead of "invalid type: null".
182            let arguments = serde_json::Value::Object(params.arguments.unwrap_or_default());
183
184            let result = self
185                .tool_registry()
186                .execute_tool(&tool_name, arguments)
187                .await;
188
189            let duration = start.elapsed();
190            let success = result.is_ok();
191
192            // Log results
193            match &result {
194                Ok(_) => {
195                    tracing::info!("Tool {} executed successfully in {:?}", tool_name, duration);
196                    if self.config.verbose_logging {
197                        tracing::debug!("Verbose: Tool execution details available");
198                    }
199                }
200                Err(e) => {
201                    tracing::error!(
202                        "Tool {} execution failed after {:?}: {:?}",
203                        tool_name,
204                        duration,
205                        e
206                    );
207                }
208            }
209
210            // Record metrics (if enabled)
211            if let Some(metrics) = &self.metrics {
212                metrics.record_request(&tool_name, success, duration);
213            }
214
215            ToolExecutionResult {
216                tool_name,
217                duration,
218                success,
219                result,
220            }
221        }
222        .instrument(span)
223        .await
224    }
225}
226
227#[async_trait]
228impl ServerHandler for CratesDocsHandler {
229    /// Handle list tools request
230    async fn handle_list_tools_request(
231        &self,
232        _request: Option<PaginatedRequestParams>,
233        _runtime: Arc<dyn McpServer>,
234    ) -> std::result::Result<ListToolsResult, RpcError> {
235        let trace_id = Uuid::new_v4().to_string();
236        let span = info_span!("list_tools", trace_id = %trace_id);
237
238        async {
239            tracing::debug!("Listing available tools");
240            let result = self.list_tools();
241            tracing::debug!("Found {} tools", result.tools.len());
242            Ok(result)
243        }
244        .instrument(span)
245        .await
246    }
247
248    /// Handle call tool request
249    async fn handle_call_tool_request(
250        &self,
251        params: CallToolRequestParams,
252        _runtime: Arc<dyn McpServer>,
253    ) -> std::result::Result<CallToolResult, CallToolError> {
254        self.execute_tool(params).await.into_call_tool_result()
255    }
256
257    /// Handle list resources request
258    async fn handle_list_resources_request(
259        &self,
260        _request: Option<PaginatedRequestParams>,
261        _runtime: Arc<dyn McpServer>,
262    ) -> std::result::Result<ListResourcesResult, RpcError> {
263        Ok(self.list_resources())
264    }
265
266    /// Handle read resource request
267    async fn handle_read_resource_request(
268        &self,
269        _params: ReadResourceRequestParams,
270        _runtime: Arc<dyn McpServer>,
271    ) -> std::result::Result<ReadResourceResult, RpcError> {
272        Err(RpcError::invalid_request().with_message("Resource not found".to_string()))
273    }
274
275    /// Handle list prompts request
276    async fn handle_list_prompts_request(
277        &self,
278        _request: Option<PaginatedRequestParams>,
279        _runtime: Arc<dyn McpServer>,
280    ) -> std::result::Result<ListPromptsResult, RpcError> {
281        Ok(self.list_prompts())
282    }
283
284    /// Handle get prompt request
285    async fn handle_get_prompt_request(
286        &self,
287        _params: GetPromptRequestParams,
288        _runtime: Arc<dyn McpServer>,
289    ) -> std::result::Result<GetPromptResult, RpcError> {
290        Err(RpcError::invalid_request().with_message("Prompt not found".to_string()))
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::AppConfig;
298
299    #[tokio::test]
300    async fn test_crates_docs_handler_execute_tool() {
301        let server = Arc::new(CratesDocsServer::new(AppConfig::default()).unwrap());
302        let handler = CratesDocsHandler::new(server);
303
304        let result = handler
305            .execute_tool(rust_mcp_sdk::schema::CallToolRequestParams {
306                arguments: Some(serde_json::Map::new()),
307                meta: None,
308                name: "health_check".to_string(),
309                task: None,
310            })
311            .await;
312
313        assert!(result.success);
314    }
315
316    #[tokio::test]
317    async fn test_handler_with_merged_config() {
318        let server = Arc::new(CratesDocsServer::new(AppConfig::default()).unwrap());
319        let base_config = HandlerConfig::default();
320        let override_config = HandlerConfig::new().with_verbose_logging();
321
322        let handler =
323            CratesDocsHandler::with_merged_config(server, base_config, Some(override_config));
324
325        assert!(handler.config().verbose_logging);
326        assert!(!handler.config().enable_metrics);
327    }
328
329    #[tokio::test]
330    async fn test_handler_with_metrics() {
331        let server = Arc::new(CratesDocsServer::new(AppConfig::default()).unwrap());
332        let metrics = Arc::new(ServerMetrics::new());
333        let handler = CratesDocsHandler::new(server).with_metrics(metrics.clone());
334
335        let result = handler
336            .execute_tool(rust_mcp_sdk::schema::CallToolRequestParams {
337                arguments: None,
338                meta: None,
339                name: "health_check".to_string(),
340                task: None,
341            })
342            .await;
343
344        // A tool call that omits `arguments` is valid per the MCP spec; a
345        // tool whose parameters are all optional (health_check) must run
346        // successfully with its defaults rather than failing to deserialize.
347        assert!(
348            result.success && result.result.is_ok(),
349            "health_check with omitted arguments should succeed: {result:?}"
350        );
351
352        // Verify metrics are recorded
353        let metrics_output = metrics.export().unwrap();
354        assert!(metrics_output.contains("mcp_requests_total"));
355    }
356
357    #[tokio::test]
358    async fn test_handler_list_methods() {
359        let server = Arc::new(CratesDocsServer::new(AppConfig::default()).unwrap());
360        let handler = CratesDocsHandler::new(server);
361
362        let tools = handler.list_tools();
363        assert!(!tools.tools.is_empty());
364        assert_eq!(tools.tools.len(), 4); // 4 default tools
365
366        let resources = handler.list_resources();
367        assert!(resources.resources.is_empty());
368
369        let prompts = handler.list_prompts();
370        assert!(prompts.prompts.is_empty());
371    }
372}