use crate::context::Context;
use crate::handler::ToolHandler;
use mcpkit_core::error::McpError;
use mcpkit_core::types::tool::{Tool, ToolOutput};
use serde_json::Value;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
pub type BoxedToolFn = Box<
dyn for<'a> Fn(
Value,
&'a Context<'a>,
)
-> Pin<Box<dyn Future<Output = Result<ToolOutput, McpError>> + Send + 'a>>
+ Send
+ Sync,
>;
pub struct RegisteredTool {
pub tool: Tool,
pub handler: BoxedToolFn,
}
pub struct ToolService {
tools: HashMap<String, RegisteredTool>,
}
impl Default for ToolService {
fn default() -> Self {
Self::new()
}
}
impl ToolService {
#[must_use]
pub fn new() -> Self {
Self {
tools: HashMap::new(),
}
}
pub fn register<F, Fut>(&mut self, tool: Tool, handler: F)
where
F: Fn(Value, &Context<'_>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<ToolOutput, McpError>> + Send + 'static,
{
let name = tool.name.clone();
let boxed: BoxedToolFn = Box::new(move |args, ctx| Box::pin(handler(args, ctx)));
self.tools.insert(
name,
RegisteredTool {
tool,
handler: boxed,
},
);
}
pub fn register_arc<H>(&mut self, tool: Tool, handler: Arc<H>)
where
H: for<'a> Fn(
Value,
&'a Context<'a>,
)
-> Pin<Box<dyn Future<Output = Result<ToolOutput, McpError>> + Send + 'a>>
+ Send
+ Sync
+ 'static,
{
let name = tool.name.clone();
let boxed: BoxedToolFn = Box::new(move |args, ctx| (handler)(args, ctx));
self.tools.insert(
name,
RegisteredTool {
tool,
handler: boxed,
},
);
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&RegisteredTool> {
self.tools.get(name)
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
#[must_use]
pub fn list(&self) -> Vec<&Tool> {
self.tools.values().map(|r| &r.tool).collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.tools.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.tools.is_empty()
}
pub async fn call(
&self,
name: &str,
arguments: Value,
ctx: &Context<'_>,
) -> Result<ToolOutput, McpError> {
let registered = self.tools.get(name).ok_or_else(|| {
McpError::invalid_params("tools/call", format!("Unknown tool: {name}"))
})?;
(registered.handler)(arguments, ctx).await
}
}
impl ToolHandler for ToolService {
async fn list_tools(&self, _ctx: &Context<'_>) -> Result<Vec<Tool>, McpError> {
Ok(self.list().into_iter().cloned().collect())
}
async fn call_tool(
&self,
name: &str,
arguments: Value,
ctx: &Context<'_>,
) -> Result<ToolOutput, McpError> {
self.call(name, arguments, ctx).await
}
}
pub struct ToolBuilder {
name: String,
description: Option<String>,
input_schema: Value,
destructive: Option<bool>,
idempotent: Option<bool>,
read_only: Option<bool>,
}
impl ToolBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
input_schema: serde_json::json!({
"type": "object",
"properties": {},
}),
destructive: None,
idempotent: None,
read_only: None,
}
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
#[must_use]
pub fn input_schema(mut self, schema: Value) -> Self {
self.input_schema = schema;
self
}
#[must_use]
pub fn destructive(mut self, value: bool) -> Self {
self.destructive = Some(value);
self
}
#[must_use]
pub fn idempotent(mut self, value: bool) -> Self {
self.idempotent = Some(value);
self
}
#[must_use]
pub fn read_only(mut self, value: bool) -> Self {
self.read_only = Some(value);
self
}
#[must_use]
pub fn build(self) -> Tool {
let has_annotations =
self.destructive.is_some() || self.idempotent.is_some() || self.read_only.is_some();
let annotations = if has_annotations {
Some(mcpkit_core::types::tool::ToolAnnotations {
title: None,
read_only_hint: self.read_only.or(Some(false)),
destructive_hint: self.destructive.or(Some(false)),
idempotent_hint: self.idempotent.or(Some(false)),
open_world_hint: None,
})
} else {
None
};
Tool {
name: self.name,
description: self.description,
input_schema: self.input_schema,
annotations,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::{Context, NoOpPeer};
use mcpkit_core::capability::{ClientCapabilities, ServerCapabilities};
use mcpkit_core::protocol::RequestId;
use mcpkit_core::protocol_version::ProtocolVersion;
use mcpkit_core::types::tool::CallToolResult;
fn make_context() -> (
RequestId,
ClientCapabilities,
ServerCapabilities,
ProtocolVersion,
NoOpPeer,
) {
(
RequestId::Number(1),
ClientCapabilities::default(),
ServerCapabilities::default(),
ProtocolVersion::LATEST,
NoOpPeer,
)
}
#[test]
fn test_tool_builder() {
let tool = ToolBuilder::new("test")
.description("A test tool")
.input_schema(serde_json::json!({
"type": "object",
"properties": {
"query": { "type": "string" }
}
}))
.build();
assert_eq!(tool.name, "test");
assert_eq!(tool.description.as_deref(), Some("A test tool"));
}
#[tokio::test]
async fn test_tool_service() -> Result<(), Box<dyn std::error::Error>> {
let mut service = ToolService::new();
let tool = ToolBuilder::new("echo")
.description("Echo back input")
.build();
service.register(tool, |args, _ctx| async move {
Ok(ToolOutput::text(args.to_string()))
});
assert!(service.contains("echo"));
assert_eq!(service.len(), 1);
let (req_id, client_caps, server_caps, protocol_version, peer) = make_context();
let ctx = Context::new(
&req_id,
None,
&client_caps,
&server_caps,
protocol_version,
&peer,
);
let result = service
.call("echo", serde_json::json!({"hello": "world"}), &ctx)
.await?;
let call_result: CallToolResult = result.into();
assert!(!call_result.content.is_empty());
Ok(())
}
}