1use std::collections::HashMap;
2use std::sync::Arc;
3
4use rmcp::handler::server::ServerHandler;
5use rmcp::model::{
6 CallToolRequestParams, CallToolResult, Content, Implementation, ListToolsResult,
7 PaginatedRequestParams, ServerCapabilities, ServerInfo, Tool as RmcpTool,
8};
9use rmcp::service::RequestContext;
10use rmcp::{ErrorData as McpError, RoleServer};
11
12use crate::trait_def::DynTool;
13
14pub struct ToolServer {
18 tools: HashMap<String, Box<dyn DynTool>>,
19 server_info: ServerInfo,
20}
21
22impl ToolServer {
23 pub fn new(tools: Vec<Box<dyn DynTool>>) -> Self {
25 let map: HashMap<String, Box<dyn DynTool>> = tools
26 .into_iter()
27 .map(|t| (t.name().to_string(), t))
28 .collect();
29
30 let mut server_info = ServerInfo::new(ServerCapabilities::builder().enable_tools().build());
31 server_info.server_info = Implementation::new("anyclaw-sdk-tool", "0.1.0");
32
33 Self {
34 tools: map,
35 server_info,
36 }
37 }
38
39 pub async fn serve_stdio(self) -> Result<(), Box<dyn std::error::Error>> {
41 use rmcp::ServiceExt;
42 let service = self
43 .serve((tokio::io::stdin(), tokio::io::stdout()))
44 .await?;
45 service.waiting().await?;
46 Ok(())
47 }
48
49 pub fn build_tool_list(&self) -> Vec<RmcpTool> {
54 self.tools
55 .values()
56 .map(|t| {
57 let name = t.name().to_string();
58 let desc = t.description().to_string();
59 let schema = t.input_schema();
60 let schema_obj: serde_json::Map<String, serde_json::Value> = match schema {
61 serde_json::Value::Object(m) => m,
62 _ => serde_json::Map::new(),
63 };
64 RmcpTool::new(name, desc, Arc::new(schema_obj))
65 })
66 .collect()
67 }
68
69 pub async fn dispatch_tool(
79 &self,
80 name: &str,
81 arguments: Option<serde_json::Map<String, serde_json::Value>>,
82 ) -> Result<CallToolResult, McpError> {
83 let tool = self
84 .tools
85 .get(name)
86 .ok_or_else(|| McpError::invalid_params(format!("unknown tool: {name}"), None))?;
87
88 let input = arguments
89 .map(serde_json::Value::Object)
90 .unwrap_or(serde_json::Value::Null);
91
92 match tool.execute(input).await {
93 Ok(output) => {
94 let text = match output {
95 serde_json::Value::String(s) => s,
96 other => serde_json::to_string(&other).unwrap_or_else(|e| {
97 tracing::warn!(error = %e, "failed to serialize tool output to string, using empty string");
98 String::default()
99 }),
100 };
101 Ok(CallToolResult::success(vec![Content::text(text)]))
102 }
103 Err(e) => Ok(CallToolResult::error(vec![Content::text(e.to_string())])),
104 }
105 }
106}
107
108impl ServerHandler for ToolServer {
109 fn get_info(&self) -> ServerInfo {
110 self.server_info.clone()
111 }
112
113 async fn list_tools(
114 &self,
115 _request: Option<PaginatedRequestParams>,
116 _context: RequestContext<RoleServer>,
117 ) -> Result<ListToolsResult, McpError> {
118 Ok(ListToolsResult::with_all_items(self.build_tool_list()))
119 }
120
121 async fn call_tool(
122 &self,
123 request: CallToolRequestParams,
124 _context: RequestContext<RoleServer>,
125 ) -> Result<CallToolResult, McpError> {
126 self.dispatch_tool(request.name.as_ref(), request.arguments)
127 .await
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::error::ToolSdkError;
135 use crate::trait_def::Tool;
136 use rstest::rstest;
137
138 struct EchoTool;
139
140 impl Tool for EchoTool {
141 fn name(&self) -> &str {
142 "echo"
143 }
144 fn description(&self) -> &str {
145 "Echoes input back"
146 }
147 fn input_schema(&self) -> serde_json::Value {
148 serde_json::json!({
149 "type": "object",
150 "properties": { "message": {"type": "string"} },
151 "required": ["message"]
152 })
153 }
154 async fn execute(
155 &self,
156 input: serde_json::Value,
157 ) -> Result<serde_json::Value, ToolSdkError> {
158 Ok(input)
159 }
160 }
161
162 struct FailTool;
163
164 impl Tool for FailTool {
165 fn name(&self) -> &str {
166 "fail"
167 }
168 fn description(&self) -> &str {
169 "Always fails"
170 }
171 fn input_schema(&self) -> serde_json::Value {
172 serde_json::json!({"type": "object"})
173 }
174 async fn execute(
175 &self,
176 _input: serde_json::Value,
177 ) -> Result<serde_json::Value, ToolSdkError> {
178 Err(ToolSdkError::ExecutionFailed("intentional failure".into()))
179 }
180 }
181
182 #[test]
183 fn when_tool_server_constructed_with_tools_then_tools_registered() {
184 let tools: Vec<Box<dyn DynTool>> = vec![Box::new(EchoTool)];
185 let server = ToolServer::new(tools);
186 assert_eq!(server.tools.len(), 1);
187 }
188
189 #[test]
190 fn when_tool_list_built_then_contains_all_registered_tools_with_metadata() {
191 let tools: Vec<Box<dyn DynTool>> = vec![Box::new(EchoTool), Box::new(FailTool)];
192 let server = ToolServer::new(tools);
193 let list = server.build_tool_list();
194 assert_eq!(list.len(), 2);
195 let names: Vec<&str> = list.iter().map(|t| t.name.as_ref()).collect();
196 assert!(names.contains(&"echo"));
197 assert!(names.contains(&"fail"));
198 let echo = list.iter().find(|t| t.name.as_ref() == "echo").unwrap();
199 assert_eq!(echo.description.as_deref(), Some("Echoes input back"));
200 }
201
202 #[tokio::test]
203 async fn when_known_tool_dispatched_then_returns_successful_result() {
204 let tools: Vec<Box<dyn DynTool>> = vec![Box::new(EchoTool)];
205 let server = ToolServer::new(tools);
206
207 let mut args = serde_json::Map::new();
208 args.insert("message".into(), serde_json::json!("hello"));
209
210 let result = server.dispatch_tool("echo", Some(args)).await.unwrap();
211 assert!(result.is_error.is_none() || result.is_error == Some(false));
212 assert!(!result.content.is_empty());
213 }
214
215 #[tokio::test]
216 async fn when_unknown_tool_dispatched_then_returns_invalid_params_error() {
217 let tools: Vec<Box<dyn DynTool>> = vec![Box::new(EchoTool)];
218 let server = ToolServer::new(tools);
219
220 let result = server.dispatch_tool("nonexistent", None).await;
221 assert!(result.is_err());
222 }
223
224 #[tokio::test]
225 async fn when_tool_execute_returns_error_then_dispatch_returns_error_result() {
226 let tools: Vec<Box<dyn DynTool>> = vec![Box::new(FailTool)];
227 let server = ToolServer::new(tools);
228
229 let result = server.dispatch_tool("fail", None).await.unwrap();
230 assert_eq!(result.is_error, Some(true));
231 }
232
233 #[test]
234 fn when_tool_sdk_error_checked_then_implements_std_error() {
235 let err = ToolSdkError::ExecutionFailed("test".into());
236 let _: &dyn std::error::Error = &err;
237 assert!(err.to_string().contains("test"));
238 }
239
240 #[test]
241 fn when_tool_impl_created_then_name_description_and_schema_accessible() {
242 use crate::trait_def::Tool;
243 let tool = EchoTool;
244 assert_eq!(Tool::name(&tool), "echo");
245 assert_eq!(Tool::description(&tool), "Echoes input back");
246 let schema = Tool::input_schema(&tool);
247 assert!(schema.is_object());
248 }
249
250 #[test]
251 fn when_tool_cast_to_dyn_trait_object_then_compiles() {
252 let _tool: Box<dyn DynTool> = Box::new(EchoTool);
253 }
254
255 struct StaticTool {
256 tool_name: &'static str,
257 payload: &'static str,
258 }
259
260 impl Tool for StaticTool {
261 fn name(&self) -> &str {
262 self.tool_name
263 }
264
265 fn description(&self) -> &str {
266 "Returns a static payload"
267 }
268
269 fn input_schema(&self) -> serde_json::Value {
270 serde_json::json!({"type": "object"})
271 }
272
273 async fn execute(
274 &self,
275 _input: serde_json::Value,
276 ) -> Result<serde_json::Value, ToolSdkError> {
277 Ok(serde_json::json!({"tool": self.payload}))
278 }
279 }
280
281 #[rstest]
282 #[test]
283 fn when_tool_server_new_called_then_tools_are_registered_by_name() {
284 let tools: Vec<Box<dyn DynTool>> = vec![
285 Box::new(StaticTool {
286 tool_name: "alpha",
287 payload: "A",
288 }),
289 Box::new(StaticTool {
290 tool_name: "beta",
291 payload: "B",
292 }),
293 ];
294 let server = ToolServer::new(tools);
295
296 assert_eq!(server.tools.len(), 2);
297 assert!(server.tools.contains_key("alpha"));
298 assert!(server.tools.contains_key("beta"));
299 }
300
301 #[rstest]
302 #[tokio::test]
303 async fn when_tool_server_dispatches_by_name_then_matching_tool_handles_request() {
304 let tools: Vec<Box<dyn DynTool>> = vec![
305 Box::new(StaticTool {
306 tool_name: "alpha",
307 payload: "A",
308 }),
309 Box::new(StaticTool {
310 tool_name: "beta",
311 payload: "B",
312 }),
313 ];
314 let server = ToolServer::new(tools);
315
316 let result = server.dispatch_tool("beta", None).await.unwrap();
317
318 assert_eq!(result.is_error, Some(false));
319 assert_eq!(
320 result.content,
321 vec![Content::text(r#"{"tool":"B"}"#.to_string())]
322 );
323 }
324
325 #[rstest]
326 #[tokio::test]
327 async fn when_tool_server_dispatches_unknown_name_then_invalid_params_includes_name() {
328 let tools: Vec<Box<dyn DynTool>> = vec![Box::new(EchoTool)];
329 let server = ToolServer::new(tools);
330
331 let error = server
332 .dispatch_tool("missing-tool", None)
333 .await
334 .unwrap_err();
335
336 assert!(error.message.contains("unknown tool: missing-tool"));
337 }
338}