crates_docs/server/handler/
core.rs1use 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
16pub struct HandlerCore {
27 server: Arc<CratesDocsServer>,
28 config: HandlerConfig,
29 metrics: Option<Arc<ServerMetrics>>,
30}
31
32impl HandlerCore {
33 #[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 #[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 #[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 #[must_use]
73 pub fn with_metrics(self, metrics: Arc<ServerMetrics>) -> Self {
74 Self {
75 metrics: Some(metrics),
76 ..self
77 }
78 }
79
80 #[must_use]
82 pub fn server(&self) -> &Arc<CratesDocsServer> {
83 &self.server
84 }
85
86 #[must_use]
88 pub fn tool_registry(&self) -> &ToolRegistry {
89 self.server.tool_registry()
90 }
91
92 #[must_use]
94 pub fn config(&self) -> &HandlerConfig {
95 &self.config
96 }
97
98 #[must_use]
100 pub fn metrics(&self) -> Option<&Arc<ServerMetrics>> {
101 self.metrics.as_ref()
102 }
103
104 #[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 #[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 #[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 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 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 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 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); 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}