crates_docs/server/handler/
standard.rs1use 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
23pub struct CratesDocsHandler {
33 server: Arc<CratesDocsServer>,
34 config: HandlerConfig,
35 metrics: Option<Arc<ServerMetrics>>,
36}
37
38impl CratesDocsHandler {
39 #[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 #[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 #[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 #[must_use]
91 pub fn with_metrics(self, metrics: Arc<ServerMetrics>) -> Self {
92 Self {
93 metrics: Some(metrics),
94 ..self
95 }
96 }
97
98 #[must_use]
100 pub fn server(&self) -> &Arc<CratesDocsServer> {
101 &self.server
102 }
103
104 #[must_use]
106 pub fn tool_registry(&self) -> &ToolRegistry {
107 self.server.tool_registry()
108 }
109
110 #[must_use]
112 pub fn config(&self) -> &HandlerConfig {
113 &self.config
114 }
115
116 #[must_use]
118 pub fn metrics(&self) -> Option<&Arc<ServerMetrics>> {
119 self.metrics.as_ref()
120 }
121
122 #[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 #[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 #[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 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 = 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 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 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 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 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 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 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 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 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 assert!(
348 result.success && result.result.is_ok(),
349 "health_check with omitted arguments should succeed: {result:?}"
350 );
351
352 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); 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}