Skip to main content

mcpkit_server/capability/
tools.rs

1//! Tool capability implementation.
2//!
3//! This module provides utilities for managing and executing tools
4//! in an MCP server.
5
6use crate::context::Context;
7use crate::handler::ToolHandler;
8use mcpkit_core::error::McpError;
9use mcpkit_core::types::Object;
10use mcpkit_core::types::tool::{Tool, ToolOutput};
11use serde_json::Value;
12use std::collections::HashMap;
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16
17/// A boxed async function for tool execution.
18pub type BoxedToolFn = Box<
19    dyn for<'a> Fn(
20            Object,
21            &'a Context<'a>,
22        )
23            -> Pin<Box<dyn Future<Output = Result<ToolOutput, McpError>> + Send + 'a>>
24        + Send
25        + Sync,
26>;
27
28/// A registered tool with metadata and handler.
29pub struct RegisteredTool {
30    /// Tool metadata.
31    pub tool: Tool,
32    /// Handler function.
33    pub handler: BoxedToolFn,
34}
35
36/// Service for managing tools.
37///
38/// This provides a registry for tools and handles dispatching
39/// tool calls to the appropriate handlers.
40pub struct ToolService {
41    tools: HashMap<String, RegisteredTool>,
42}
43
44impl Default for ToolService {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50impl ToolService {
51    /// Create a new empty tool service.
52    #[must_use]
53    pub fn new() -> Self {
54        Self {
55            tools: HashMap::new(),
56        }
57    }
58
59    /// Register a tool with a handler function.
60    pub fn register<F, Fut>(&mut self, tool: Tool, handler: F)
61    where
62        F: Fn(Object, &Context<'_>) -> Fut + Send + Sync + 'static,
63        Fut: Future<Output = Result<ToolOutput, McpError>> + Send + 'static,
64    {
65        let name = tool.name.clone();
66        let boxed: BoxedToolFn = Box::new(move |args, ctx| Box::pin(handler(args, ctx)));
67        self.tools.insert(
68            name,
69            RegisteredTool {
70                tool,
71                handler: boxed,
72            },
73        );
74    }
75
76    /// Register a tool with an Arc'd handler (for shared state).
77    pub fn register_arc<H>(&mut self, tool: Tool, handler: Arc<H>)
78    where
79        H: for<'a> Fn(
80                Object,
81                &'a Context<'a>,
82            )
83                -> Pin<Box<dyn Future<Output = Result<ToolOutput, McpError>> + Send + 'a>>
84            + Send
85            + Sync
86            + 'static,
87    {
88        let name = tool.name.clone();
89        let boxed: BoxedToolFn = Box::new(move |args, ctx| (handler)(args, ctx));
90        self.tools.insert(
91            name,
92            RegisteredTool {
93                tool,
94                handler: boxed,
95            },
96        );
97    }
98
99    /// Get a tool by name.
100    #[must_use]
101    pub fn get(&self, name: &str) -> Option<&RegisteredTool> {
102        self.tools.get(name)
103    }
104
105    /// Check if a tool exists.
106    #[must_use]
107    pub fn contains(&self, name: &str) -> bool {
108        self.tools.contains_key(name)
109    }
110
111    /// Get all registered tools.
112    #[must_use]
113    pub fn list(&self) -> Vec<&Tool> {
114        self.tools.values().map(|r| &r.tool).collect()
115    }
116
117    /// Get the number of registered tools.
118    #[must_use]
119    pub fn len(&self) -> usize {
120        self.tools.len()
121    }
122
123    /// Check if the service has no tools.
124    #[must_use]
125    pub fn is_empty(&self) -> bool {
126        self.tools.is_empty()
127    }
128
129    /// Call a tool by name.
130    pub async fn call(
131        &self,
132        name: &str,
133        arguments: Object,
134        ctx: &Context<'_>,
135    ) -> Result<ToolOutput, McpError> {
136        let registered = self.tools.get(name).ok_or_else(|| {
137            McpError::invalid_params("tools/call", format!("Unknown tool: {name}"))
138        })?;
139
140        (registered.handler)(arguments, ctx).await
141    }
142}
143
144impl ToolHandler for ToolService {
145    async fn list_tools(&self, _ctx: &Context<'_>) -> Result<Vec<Tool>, McpError> {
146        Ok(self.list().into_iter().cloned().collect())
147    }
148
149    async fn call_tool(
150        &self,
151        name: &str,
152        arguments: Object,
153        ctx: &Context<'_>,
154    ) -> Result<ToolOutput, McpError> {
155        self.call(name, arguments, ctx).await
156    }
157}
158
159/// Builder for creating tools with a fluent API.
160pub struct ToolBuilder {
161    name: String,
162    description: Option<String>,
163    input_schema: Value,
164    destructive: Option<bool>,
165    idempotent: Option<bool>,
166    read_only: Option<bool>,
167}
168
169impl ToolBuilder {
170    /// Create a new tool builder.
171    pub fn new(name: impl Into<String>) -> Self {
172        Self {
173            name: name.into(),
174            description: None,
175            input_schema: serde_json::json!({
176                "type": "object",
177                "properties": {},
178            }),
179            destructive: None,
180            idempotent: None,
181            read_only: None,
182        }
183    }
184
185    /// Set the tool description.
186    pub fn description(mut self, desc: impl Into<String>) -> Self {
187        self.description = Some(desc.into());
188        self
189    }
190
191    /// Set the input schema.
192    #[must_use]
193    pub fn input_schema(mut self, schema: Value) -> Self {
194        self.input_schema = schema;
195        self
196    }
197
198    /// Mark this tool as destructive.
199    ///
200    /// Destructive tools modify data or state in ways that cannot be easily undone.
201    /// When set to true, clients should warn users before executing.
202    #[must_use]
203    pub fn destructive(mut self, value: bool) -> Self {
204        self.destructive = Some(value);
205        self
206    }
207
208    /// Mark this tool as idempotent.
209    ///
210    /// Idempotent tools produce the same result when called multiple times
211    /// with the same arguments.
212    #[must_use]
213    pub fn idempotent(mut self, value: bool) -> Self {
214        self.idempotent = Some(value);
215        self
216    }
217
218    /// Mark this tool as read-only.
219    ///
220    /// Read-only tools do not modify any data or state.
221    #[must_use]
222    pub fn read_only(mut self, value: bool) -> Self {
223        self.read_only = Some(value);
224        self
225    }
226
227    /// Build the tool.
228    #[must_use]
229    pub fn build(self) -> Tool {
230        let has_annotations =
231            self.destructive.is_some() || self.idempotent.is_some() || self.read_only.is_some();
232
233        let annotations = if has_annotations {
234            Some(mcpkit_core::types::tool::ToolAnnotations {
235                title: None,
236                read_only_hint: self.read_only.or(Some(false)),
237                destructive_hint: self.destructive.or(Some(false)),
238                idempotent_hint: self.idempotent.or(Some(false)),
239                open_world_hint: None,
240            })
241        } else {
242            None
243        };
244
245        Tool {
246            name: self.name,
247            title: None,
248            description: self.description,
249            input_schema: self.input_schema,
250            icons: None,
251            annotations,
252            execution: None,
253            output_schema: None,
254            meta: None,
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use crate::context::{Context, NoOpPeer};
263    use mcpkit_core::capability::{ClientCapabilities, ServerCapabilities};
264    use mcpkit_core::protocol::RequestId;
265    use mcpkit_core::protocol_version::ProtocolVersion;
266    use mcpkit_core::types::tool::CallToolResult;
267
268    fn make_context() -> (
269        RequestId,
270        ClientCapabilities,
271        ServerCapabilities,
272        ProtocolVersion,
273        NoOpPeer,
274    ) {
275        (
276            RequestId::Number(1),
277            ClientCapabilities::default(),
278            ServerCapabilities::default(),
279            ProtocolVersion::LATEST,
280            NoOpPeer,
281        )
282    }
283
284    #[test]
285    fn test_tool_builder() {
286        let tool = ToolBuilder::new("test")
287            .description("A test tool")
288            .input_schema(serde_json::json!({
289                "type": "object",
290                "properties": {
291                    "query": { "type": "string" }
292                }
293            }))
294            .build();
295
296        assert_eq!(tool.name, "test");
297        assert_eq!(tool.description.as_deref(), Some("A test tool"));
298    }
299
300    #[tokio::test]
301    async fn test_tool_service() -> Result<(), Box<dyn std::error::Error>> {
302        let mut service = ToolService::new();
303
304        let tool = ToolBuilder::new("echo")
305            .description("Echo back input")
306            .build();
307
308        service.register(tool, |args, _ctx| async move {
309            Ok(ToolOutput::text(Value::Object(args).to_string()))
310        });
311
312        assert!(service.contains("echo"));
313        assert_eq!(service.len(), 1);
314
315        let (req_id, client_caps, server_caps, protocol_version, peer) = make_context();
316        let ctx = Context::new(
317            &req_id,
318            None,
319            &client_caps,
320            &server_caps,
321            protocol_version,
322            &peer,
323        );
324
325        let result = service
326            .call(
327                "echo",
328                serde_json::from_value(serde_json::json!({"hello": "world"}))?,
329                &ctx,
330            )
331            .await?;
332
333        // Convert to CallToolResult to check content
334        let call_result: CallToolResult = result.into();
335        assert!(!call_result.content.is_empty());
336
337        Ok(())
338    }
339}