Skip to main content

anyclaw_sdk_tool/
server.rs

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
14/// MCP tool server that registers [`DynTool`] implementations and serves them over stdio.
15///
16/// Implements rmcp's `ServerHandler` to handle `list_tools` and `call_tool` MCP methods.
17pub struct ToolServer {
18    tools: HashMap<String, Box<dyn DynTool>>,
19    server_info: ServerInfo,
20}
21
22impl ToolServer {
23    /// Create a new server from a list of tools, registering each by name.
24    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    /// Run the MCP server over stdin/stdout until the client disconnects.
40    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    /// Convert all registered tools into rmcp [`RmcpTool`] descriptors for `list_tools`.
50    ///
51    /// D-03: `input_schema()` returns `serde_json::Value` (JSON Schema has no fixed Rust type),
52    /// so the conversion to rmcp's `Map<String, Value>` is inherently Value-based.
53    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    /// Dispatch a tool call by name, returning an MCP `CallToolResult`.
70    ///
71    /// Returns an MCP protocol error only for unknown tool names; execution
72    /// failures are returned as `CallToolResult::error` (content-level).
73    ///
74    /// D-03: `arguments` is `Map<String, Value>` from rmcp's `CallToolRequestParam` —
75    /// tool input shape is defined by the tool's JSON Schema, not a static Rust type.
76    /// The conversion to `Value::Object` and pattern matching on `Value::String` in the
77    /// result both flow from the Tool trait being Value-based (D-03).
78    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}