Skip to main content

fastmcp_rs/
tool.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5use indexmap::IndexMap;
6use parking_lot::RwLock;
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value};
9use tracing::{trace, warn};
10use uuid::Uuid;
11
12use crate::error::{FastMcpError, Result};
13
14/// Describes how to handle duplicate registrations.
15#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
16pub enum DuplicateBehavior {
17    /// Returns an error when a duplicate is registered (default).
18    #[default]
19    Error,
20    /// Replaces the previously registered component.
21    Replace,
22    /// Keeps the existing component and ignores the new one.
23    Ignore,
24    /// Replaces the component but emits a warning.
25    Warn,
26}
27
28/// Represents contextual information passed to tool handlers.
29#[derive(Clone, Debug)]
30pub struct InvocationContext {
31    pub tool_name: String,
32    pub request_id: Uuid,
33    pub timestamp: DateTime<Utc>,
34    pub metadata: Map<String, Value>,
35}
36
37impl InvocationContext {
38    pub fn new(tool_name: impl Into<String>) -> Self {
39        Self {
40            tool_name: tool_name.into(),
41            request_id: Uuid::new_v4(),
42            timestamp: Utc::now(),
43            metadata: Map::new(),
44        }
45    }
46}
47
48/// Additional tool metadata surfaced to clients.
49pub type ToolAnnotations = Map<String, Value>;
50
51fn annotations_is_empty(annotations: &ToolAnnotations) -> bool {
52    annotations.is_empty()
53}
54
55/// Response payload produced by tool handlers.
56#[derive(Clone, Debug, Serialize, Deserialize)]
57pub struct ToolResponse {
58    pub content: Vec<Value>,
59    #[serde(default, skip_serializing_if = "annotations_is_empty")]
60    pub annotations: ToolAnnotations,
61}
62
63impl ToolResponse {
64    pub fn new(content: Vec<Value>) -> Self {
65        Self {
66            content,
67            annotations: ToolAnnotations::default(),
68        }
69    }
70
71    pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
72        self.annotations = annotations;
73        self
74    }
75}
76
77/// Metadata describing a tool without the handler.
78#[derive(Clone, Debug, Serialize, Deserialize)]
79pub struct ToolDefinitionMetadata {
80    pub name: String,
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub description: Option<String>,
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub summary: Option<String>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub parameters: Option<Value>,
87    #[serde(default, skip_serializing_if = "annotations_is_empty")]
88    pub annotations: ToolAnnotations,
89}
90
91/// Trait implemented by tool handlers.
92#[async_trait]
93pub trait ToolInvocation: Send + Sync {
94    async fn invoke(&self, ctx: InvocationContext, arguments: Value) -> Result<ToolResponse>;
95}
96
97#[async_trait]
98impl<F, Fut> ToolInvocation for F
99where
100    F: Send + Sync + Fn(InvocationContext, Value) -> Fut,
101    Fut: std::future::Future<Output = Result<ToolResponse>> + Send,
102{
103    async fn invoke(&self, ctx: InvocationContext, arguments: Value) -> Result<ToolResponse> {
104        (self)(ctx, arguments).await
105    }
106}
107
108/// Full tool definition including the invocation handler.
109pub struct ToolDefinition {
110    pub name: String,
111    pub description: Option<String>,
112    pub summary: Option<String>,
113    pub parameters: Option<Value>,
114    pub annotations: ToolAnnotations,
115    handler: Arc<dyn ToolInvocation>,
116}
117
118impl ToolDefinition {
119    pub fn new(name: impl Into<String>, handler: impl ToolInvocation + 'static) -> Self {
120        Self {
121            name: name.into(),
122            description: None,
123            summary: None,
124            parameters: None,
125            annotations: ToolAnnotations::default(),
126            handler: Arc::new(handler),
127        }
128    }
129
130    pub fn with_description(mut self, description: impl Into<String>) -> Self {
131        self.description = Some(description.into());
132        self
133    }
134
135    pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
136        self.summary = Some(summary.into());
137        self
138    }
139
140    pub fn with_parameters(mut self, parameters: Value) -> Self {
141        self.parameters = Some(parameters);
142        self
143    }
144
145    pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
146        self.annotations = annotations;
147        self
148    }
149
150    pub(crate) fn metadata(&self) -> ToolDefinitionMetadata {
151        ToolDefinitionMetadata {
152            name: self.name.clone(),
153            description: self.description.clone(),
154            summary: self.summary.clone(),
155            parameters: self.parameters.clone(),
156            annotations: self.annotations.clone(),
157        }
158    }
159
160    pub(crate) fn handler(&self) -> Arc<dyn ToolInvocation> {
161        Arc::clone(&self.handler)
162    }
163}
164
165/// Registry that stores and invokes tool definitions.
166pub struct ToolManager {
167    duplicate_behavior: DuplicateBehavior,
168    tools: RwLock<IndexMap<String, Arc<ToolDefinition>>>,
169}
170
171impl ToolManager {
172    pub fn new(duplicate_behavior: DuplicateBehavior) -> Self {
173        Self {
174            duplicate_behavior,
175            tools: RwLock::new(IndexMap::new()),
176        }
177    }
178
179    pub fn len(&self) -> usize {
180        self.tools.read().len()
181    }
182
183    pub fn is_empty(&self) -> bool {
184        self.len() == 0
185    }
186
187    pub fn register(&self, tool: ToolDefinition) -> Result<()> {
188        let mut guard = self.tools.write();
189        match guard.get_mut(&tool.name) {
190            Some(existing) => match self.duplicate_behavior {
191                DuplicateBehavior::Error => {
192                    return Err(FastMcpError::DuplicateTool(tool.name));
193                }
194                DuplicateBehavior::Ignore => {
195                    trace!("Ignoring duplicate registration for tool {}", tool.name);
196                }
197                DuplicateBehavior::Replace => {
198                    trace!("Replacing tool {}", tool.name);
199                    *existing = Arc::new(tool);
200                }
201                DuplicateBehavior::Warn => {
202                    warn!("Replacing duplicate tool {}", tool.name);
203                    *existing = Arc::new(tool);
204                }
205            },
206            None => {
207                guard.insert(tool.name.clone(), Arc::new(tool));
208            }
209        }
210        Ok(())
211    }
212
213    pub fn list(&self) -> Vec<ToolDefinitionMetadata> {
214        self.tools
215            .read()
216            .values()
217            .map(|tool| tool.metadata())
218            .collect()
219    }
220
221    pub fn get(&self, name: &str) -> Option<ToolDefinitionMetadata> {
222        self.tools.read().get(name).map(|tool| tool.metadata())
223    }
224
225    pub fn contains(&self, name: &str) -> bool {
226        self.tools.read().contains_key(name)
227    }
228
229    pub async fn call(&self, name: &str, arguments: Value) -> Result<ToolResponse> {
230        let tool = {
231            let guard = self.tools.read();
232            guard
233                .get(name)
234                .cloned()
235                .ok_or_else(|| FastMcpError::ToolNotFound(name.to_string()))?
236        };
237
238        let ctx = InvocationContext::new(name.to_string());
239        tool.handler().invoke(ctx, arguments).await
240    }
241}
242
243// ===== Auto-registration support (feature-gated) =====
244#[cfg(feature = "auto-register")]
245pub type ToolFactory = fn() -> ToolDefinition;
246
247#[cfg(feature = "auto-register")]
248#[linkme::distributed_slice]
249pub static MCP_TOOL_FACTORIES: [ToolFactory];
250
251#[cfg(feature = "auto-register")]
252pub fn register_discovered_tools(server: &crate::server::FastMcpServer) {
253    for factory in MCP_TOOL_FACTORIES {
254        if let Err(e) = server.register_tool(factory()) {
255            warn!("Auto-register tool failed: {}", e);
256        }
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use serde_json::json;
263
264    use super::*;
265
266    #[tokio::test]
267    async fn registers_and_invokes_tool() {
268        let manager = ToolManager::new(DuplicateBehavior::Error);
269
270        manager
271            .register(
272                ToolDefinition::new("greet", |_, payload: Value| async move {
273                    let name = payload
274                        .get("name")
275                        .and_then(Value::as_str)
276                        .unwrap_or("world");
277                    Ok(ToolResponse::new(vec![json!({
278                        "type": "text",
279                        "text": format!("Hello, {name}!"),
280                    })]))
281                })
282                .with_description("Greets a user"),
283            )
284            .unwrap();
285
286        let response = manager
287            .call("greet", json!({ "name": "FastMCP" }))
288            .await
289            .unwrap();
290
291        assert_eq!(response.content.len(), 1);
292        assert_eq!(
293            response.content[0]["text"].as_str(),
294            Some("Hello, FastMCP!")
295        );
296    }
297}