Skip to main content

crates_docs/server/handler/
core.rs

1//! Shared core handling logic
2
3use crate::metrics::ServerMetrics;
4use crate::server::CratesDocsServer;
5use crate::tools::ToolRegistry;
6use rust_mcp_sdk::schema::{
7    CallToolRequestParams, ListPromptsResult, ListResourcesResult, ListToolsResult,
8};
9use std::sync::Arc;
10use tracing::{info_span, Instrument};
11use uuid::Uuid;
12
13use super::config::HandlerConfig;
14use super::types::ToolExecutionResult;
15
16/// Shared core handling logic
17///
18/// Encapsulates all MCP request handling shared logic, eliminating duplication between `CratesDocsHandler` and
19/// `CratesDocsHandlerCore`.
20///
21/// # Design
22///
23/// - Provides core methods for tool execution, list queries, etc.
24/// - Supports optional metrics integration
25/// - Supports configuration merging
26pub struct HandlerCore {
27    server: Arc<CratesDocsServer>,
28    config: HandlerConfig,
29    metrics: Option<Arc<ServerMetrics>>,
30}
31
32impl HandlerCore {
33    /// Create new core handler
34    ///
35    /// # Arguments
36    ///
37    /// * `server` - Server instance
38    #[must_use]
39    pub fn new(server: Arc<CratesDocsServer>) -> Self {
40        Self {
41            server,
42            config: HandlerConfig::default(),
43            metrics: None,
44        }
45    }
46
47    /// Create core handler with configuration
48    #[must_use]
49    pub fn with_config(server: Arc<CratesDocsServer>, config: HandlerConfig) -> Self {
50        Self {
51            server,
52            config,
53            metrics: None,
54        }
55    }
56
57    /// Create core handler with merged configuration
58    #[must_use]
59    pub fn with_merged_config(
60        server: Arc<CratesDocsServer>,
61        base_config: HandlerConfig,
62        override_config: Option<HandlerConfig>,
63    ) -> Self {
64        Self {
65            server,
66            config: base_config.merge(override_config),
67            metrics: None,
68        }
69    }
70
71    /// Set metrics
72    #[must_use]
73    pub fn with_metrics(self, metrics: Arc<ServerMetrics>) -> Self {
74        Self {
75            metrics: Some(metrics),
76            ..self
77        }
78    }
79
80    /// Get server reference
81    #[must_use]
82    pub fn server(&self) -> &Arc<CratesDocsServer> {
83        &self.server
84    }
85
86    /// Get tool registry
87    #[must_use]
88    pub fn tool_registry(&self) -> &ToolRegistry {
89        self.server.tool_registry()
90    }
91
92    /// Get configuration
93    #[must_use]
94    pub fn config(&self) -> &HandlerConfig {
95        &self.config
96    }
97
98    /// Get metrics (optional)
99    #[must_use]
100    pub fn metrics(&self) -> Option<&Arc<ServerMetrics>> {
101        self.metrics.as_ref()
102    }
103
104    /// Get all tools list
105    #[must_use]
106    pub fn list_tools(&self) -> ListToolsResult {
107        ListToolsResult {
108            tools: self.tool_registry().get_tools(),
109            meta: None,
110            next_cursor: None,
111        }
112    }
113
114    /// Get empty resources list
115    #[must_use]
116    pub fn list_resources(&self) -> ListResourcesResult {
117        ListResourcesResult {
118            resources: vec![],
119            meta: None,
120            next_cursor: None,
121        }
122    }
123
124    /// Get empty prompts list
125    #[must_use]
126    pub fn list_prompts(&self) -> ListPromptsResult {
127        ListPromptsResult {
128            prompts: vec![],
129            meta: None,
130            next_cursor: None,
131        }
132    }
133
134    /// Execute tool call (core logic)
135    ///
136    /// This method encapsulates the complete tool execution flow:
137    /// - tracing tracking
138    /// - timing statistics
139    /// - metrics recording (if enabled)
140    ///
141    /// # Returns
142    ///
143    /// Returns `ToolExecutionResult`, can be converted to different types to adapt to different traits
144    pub async fn execute_tool(&self, params: CallToolRequestParams) -> ToolExecutionResult {
145        let trace_id = Uuid::new_v4().to_string();
146        let tool_name = params.name.clone();
147        let span = info_span!(
148            "execute_tool",
149            trace_id = %trace_id,
150            tool = %tool_name,
151            verbose = self.config.verbose_logging,
152        );
153
154        async {
155            tracing::info!("Executing tool: {}", tool_name);
156            let start = std::time::Instant::now();
157
158            let arguments = params
159                .arguments
160                .map_or_else(|| serde_json::Value::Null, serde_json::Value::Object);
161
162            let result = self
163                .tool_registry()
164                .execute_tool(&tool_name, arguments)
165                .await;
166
167            let duration = start.elapsed();
168            let success = result.is_ok();
169
170            // Log results
171            match &result {
172                Ok(_) => {
173                    tracing::info!("Tool {} executed successfully in {:?}", tool_name, duration);
174                    if self.config.verbose_logging {
175                        tracing::debug!("Verbose: Tool execution details available");
176                    }
177                }
178                Err(e) => {
179                    tracing::error!(
180                        "Tool {} execution failed after {:?}: {:?}",
181                        tool_name,
182                        duration,
183                        e
184                    );
185                }
186            }
187
188            // Record metrics (if enabled)
189            if let Some(metrics) = &self.metrics {
190                metrics.record_request(&tool_name, success, duration);
191            }
192
193            ToolExecutionResult {
194                tool_name,
195                duration,
196                success,
197                result,
198            }
199        }
200        .instrument(span)
201        .await
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::AppConfig;
209
210    #[tokio::test]
211    async fn test_handler_core_execute_tool() {
212        let server = Arc::new(CratesDocsServer::new(AppConfig::default()).unwrap());
213        let core = HandlerCore::new(server);
214
215        let result = core
216            .execute_tool(rust_mcp_sdk::schema::CallToolRequestParams {
217                arguments: Some(serde_json::Map::from_iter([(
218                    "verbose".to_string(),
219                    serde_json::Value::String("bad".to_string()),
220                )])),
221                meta: None,
222                name: "health_check".to_string(),
223                task: None,
224            })
225            .await;
226
227        assert!(!result.success);
228        assert_eq!(result.tool_name, "health_check");
229    }
230
231    #[tokio::test]
232    async fn test_handler_core_with_metrics() {
233        let server = Arc::new(CratesDocsServer::new(AppConfig::default()).unwrap());
234        let metrics = Arc::new(ServerMetrics::new());
235        let core = HandlerCore::new(server).with_metrics(metrics.clone());
236
237        let _result = core
238            .execute_tool(rust_mcp_sdk::schema::CallToolRequestParams {
239                arguments: None,
240                meta: None,
241                name: "health_check".to_string(),
242                task: None,
243            })
244            .await;
245
246        // Verify metrics are recorded
247        let metrics_output = metrics.export().unwrap();
248        assert!(metrics_output.contains("mcp_requests_total"));
249    }
250
251    #[tokio::test]
252    async fn test_handler_core_list_methods() {
253        let server = Arc::new(CratesDocsServer::new(AppConfig::default()).unwrap());
254        let core = HandlerCore::new(server);
255
256        let tools = core.list_tools();
257        assert!(!tools.tools.is_empty());
258        assert_eq!(tools.tools.len(), 4); // 4 default tools
259
260        let resources = core.list_resources();
261        assert!(resources.resources.is_empty());
262
263        let prompts = core.list_prompts();
264        assert!(prompts.prompts.is_empty());
265    }
266}