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            let arguments = params
177                .arguments
178                .map_or_else(|| serde_json::Value::Null, serde_json::Value::Object);
179
180            let result = self
181                .tool_registry()
182                .execute_tool(&tool_name, arguments)
183                .await;
184
185            let duration = start.elapsed();
186            let success = result.is_ok();
187
188            // Log results
189            match &result {
190                Ok(_) => {
191                    tracing::info!("Tool {} executed successfully in {:?}", tool_name, duration);
192                    if self.config.verbose_logging {
193                        tracing::debug!("Verbose: Tool execution details available");
194                    }
195                }
196                Err(e) => {
197                    tracing::error!(
198                        "Tool {} execution failed after {:?}: {:?}",
199                        tool_name,
200                        duration,
201                        e
202                    );
203                }
204            }
205
206            // Record metrics (if enabled)
207            if let Some(metrics) = &self.metrics {
208                metrics.record_request(&tool_name, success, duration);
209            }
210
211            ToolExecutionResult {
212                tool_name,
213                duration,
214                success,
215                result,
216            }
217        }
218        .instrument(span)
219        .await
220    }
221}
222
223#[async_trait]
224impl ServerHandler for CratesDocsHandler {
225    /// Handle list tools request
226    async fn handle_list_tools_request(
227        &self,
228        _request: Option<PaginatedRequestParams>,
229        _runtime: Arc<dyn McpServer>,
230    ) -> std::result::Result<ListToolsResult, RpcError> {
231        let trace_id = Uuid::new_v4().to_string();
232        let span = info_span!("list_tools", trace_id = %trace_id);
233
234        async {
235            tracing::debug!("Listing available tools");
236            let result = self.list_tools();
237            tracing::debug!("Found {} tools", result.tools.len());
238            Ok(result)
239        }
240        .instrument(span)
241        .await
242    }
243
244    /// Handle call tool request
245    async fn handle_call_tool_request(
246        &self,
247        params: CallToolRequestParams,
248        _runtime: Arc<dyn McpServer>,
249    ) -> std::result::Result<CallToolResult, CallToolError> {
250        self.execute_tool(params).await.into_call_tool_result()
251    }
252
253    /// Handle list resources request
254    async fn handle_list_resources_request(
255        &self,
256        _request: Option<PaginatedRequestParams>,
257        _runtime: Arc<dyn McpServer>,
258    ) -> std::result::Result<ListResourcesResult, RpcError> {
259        Ok(self.list_resources())
260    }
261
262    /// Handle read resource request
263    async fn handle_read_resource_request(
264        &self,
265        _params: ReadResourceRequestParams,
266        _runtime: Arc<dyn McpServer>,
267    ) -> std::result::Result<ReadResourceResult, RpcError> {
268        Err(RpcError::invalid_request().with_message("Resource not found".to_string()))
269    }
270
271    /// Handle list prompts request
272    async fn handle_list_prompts_request(
273        &self,
274        _request: Option<PaginatedRequestParams>,
275        _runtime: Arc<dyn McpServer>,
276    ) -> std::result::Result<ListPromptsResult, RpcError> {
277        Ok(self.list_prompts())
278    }
279
280    /// Handle get prompt request
281    async fn handle_get_prompt_request(
282        &self,
283        _params: GetPromptRequestParams,
284        _runtime: Arc<dyn McpServer>,
285    ) -> std::result::Result<GetPromptResult, RpcError> {
286        Err(RpcError::invalid_request().with_message("Prompt not found".to_string()))
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use crate::AppConfig;
294
295    #[tokio::test]
296    async fn test_crates_docs_handler_execute_tool() {
297        let server = Arc::new(CratesDocsServer::new(AppConfig::default()).unwrap());
298        let handler = CratesDocsHandler::new(server);
299
300        let result = handler
301            .execute_tool(rust_mcp_sdk::schema::CallToolRequestParams {
302                arguments: Some(serde_json::Map::new()),
303                meta: None,
304                name: "health_check".to_string(),
305                task: None,
306            })
307            .await;
308
309        assert!(result.success);
310    }
311
312    #[tokio::test]
313    async fn test_handler_with_merged_config() {
314        let server = Arc::new(CratesDocsServer::new(AppConfig::default()).unwrap());
315        let base_config = HandlerConfig::default();
316        let override_config = HandlerConfig::new().with_verbose_logging();
317
318        let handler =
319            CratesDocsHandler::with_merged_config(server, base_config, Some(override_config));
320
321        assert!(handler.config().verbose_logging);
322        assert!(!handler.config().enable_metrics);
323    }
324
325    #[tokio::test]
326    async fn test_handler_with_metrics() {
327        let server = Arc::new(CratesDocsServer::new(AppConfig::default()).unwrap());
328        let metrics = Arc::new(ServerMetrics::new());
329        let handler = CratesDocsHandler::new(server).with_metrics(metrics.clone());
330
331        let _result = handler
332            .execute_tool(rust_mcp_sdk::schema::CallToolRequestParams {
333                arguments: None,
334                meta: None,
335                name: "health_check".to_string(),
336                task: None,
337            })
338            .await;
339
340        // Verify metrics are recorded
341        let metrics_output = metrics.export().unwrap();
342        assert!(metrics_output.contains("mcp_requests_total"));
343    }
344
345    #[tokio::test]
346    async fn test_handler_list_methods() {
347        let server = Arc::new(CratesDocsServer::new(AppConfig::default()).unwrap());
348        let handler = CratesDocsHandler::new(server);
349
350        let tools = handler.list_tools();
351        assert!(!tools.tools.is_empty());
352        assert_eq!(tools.tools.len(), 4); // 4 default tools
353
354        let resources = handler.list_resources();
355        assert!(resources.resources.is_empty());
356
357        let prompts = handler.list_prompts();
358        assert!(prompts.prompts.is_empty());
359    }
360}