mcpkit_server/capability/
tools.rs1use 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
17pub 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
28pub struct RegisteredTool {
30 pub tool: Tool,
32 pub handler: BoxedToolFn,
34}
35
36pub 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 #[must_use]
53 pub fn new() -> Self {
54 Self {
55 tools: HashMap::new(),
56 }
57 }
58
59 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 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 #[must_use]
101 pub fn get(&self, name: &str) -> Option<&RegisteredTool> {
102 self.tools.get(name)
103 }
104
105 #[must_use]
107 pub fn contains(&self, name: &str) -> bool {
108 self.tools.contains_key(name)
109 }
110
111 #[must_use]
113 pub fn list(&self) -> Vec<&Tool> {
114 self.tools.values().map(|r| &r.tool).collect()
115 }
116
117 #[must_use]
119 pub fn len(&self) -> usize {
120 self.tools.len()
121 }
122
123 #[must_use]
125 pub fn is_empty(&self) -> bool {
126 self.tools.is_empty()
127 }
128
129 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
159pub 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 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 pub fn description(mut self, desc: impl Into<String>) -> Self {
187 self.description = Some(desc.into());
188 self
189 }
190
191 #[must_use]
193 pub fn input_schema(mut self, schema: Value) -> Self {
194 self.input_schema = schema;
195 self
196 }
197
198 #[must_use]
203 pub fn destructive(mut self, value: bool) -> Self {
204 self.destructive = Some(value);
205 self
206 }
207
208 #[must_use]
213 pub fn idempotent(mut self, value: bool) -> Self {
214 self.idempotent = Some(value);
215 self
216 }
217
218 #[must_use]
222 pub fn read_only(mut self, value: bool) -> Self {
223 self.read_only = Some(value);
224 self
225 }
226
227 #[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 let call_result: CallToolResult = result.into();
335 assert!(!call_result.content.is_empty());
336
337 Ok(())
338 }
339}