Skip to main content

claude_code/
sdk_mcp.rs

1//! In-process MCP (Model Context Protocol) server support.
2//!
3//! This module allows you to define custom tools that run within your Rust application
4//! and are exposed to Claude Code via the MCP protocol. These tools appear alongside
5//! Claude Code's built-in tools and can be invoked by the model during conversations.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use claude_code::{tool, create_sdk_mcp_server, McpServerConfig, ToolAnnotations};
11//! use serde_json::{json, Value};
12//!
13//! let weather_tool = tool(
14//!     "get_weather",
15//!     "Get current weather for a location",
16//!     json!({
17//!         "type": "object",
18//!         "properties": {
19//!             "location": {"type": "string", "description": "City name"}
20//!         },
21//!         "required": ["location"]
22//!     }),
23//!     |args: Value| async move {
24//!         let location = args["location"].as_str().unwrap_or("unknown");
25//!         Ok(json!({
26//!             "content": [{"type": "text", "text": format!("Weather in {location}: 22°C, sunny")}]
27//!         }))
28//!     },
29//! );
30//!
31//! let server_config = create_sdk_mcp_server("my-tools", "1.0.0", vec![weather_tool]);
32//! ```
33
34use std::collections::HashMap;
35use std::sync::Arc;
36
37use futures::future::BoxFuture;
38use serde_json::{Value, json};
39
40use crate::errors::Error;
41use crate::types::{McpSdkServerConfig, ToolAnnotations};
42
43/// Handler function type for SDK MCP tools.
44///
45/// Takes a JSON `Value` of input arguments and returns a JSON `Value` result.
46/// The result should follow the MCP tool result format with `content` array.
47pub type SdkMcpToolHandler =
48    Arc<dyn Fn(Value) -> BoxFuture<'static, std::result::Result<Value, Error>> + Send + Sync>;
49
50/// Definition of an in-process MCP tool.
51///
52/// Created via the [`tool()`] factory function. Can be customized with
53/// [`with_annotations()`](Self::with_annotations) before being passed to
54/// [`create_sdk_mcp_server()`].
55///
56/// # Fields
57///
58/// - `name` — Unique tool name (used by the model to invoke it).
59/// - `description` — Human-readable description of what the tool does.
60/// - `input_schema` — JSON Schema defining the tool's input parameters.
61/// - `handler` — Async function that executes the tool logic.
62/// - `annotations` — Optional behavioral hints (read-only, destructive, etc.).
63#[derive(Clone)]
64pub struct SdkMcpTool {
65    /// Tool name exposed to Claude.
66    pub name: String,
67    /// Human-readable tool description.
68    pub description: String,
69    /// JSON Schema for the tool arguments.
70    pub input_schema: Value,
71    /// Async handler invoked for tool calls.
72    pub handler: SdkMcpToolHandler,
73    /// Optional behavior hints for the model/runtime.
74    pub annotations: Option<ToolAnnotations>,
75}
76
77impl SdkMcpTool {
78    /// Adds behavioral annotations to this tool.
79    ///
80    /// Annotations provide hints about the tool's behavior (e.g., read-only,
81    /// destructive, idempotent) to help with permission handling.
82    ///
83    /// Returns `self` for method chaining.
84    ///
85    /// # Example
86    ///
87    /// ```rust
88    /// use claude_code::{tool, ToolAnnotations};
89    /// use serde_json::{json, Value};
90    ///
91    /// let tool = tool(
92    ///     "read_status",
93    ///     "Reads status",
94    ///     json!({"type":"object"}),
95    ///     |_args: Value| async move { Ok(json!({"content": []})) },
96    /// )
97    /// .with_annotations(ToolAnnotations {
98    ///     read_only_hint: Some(true),
99    ///     ..Default::default()
100    /// });
101    ///
102    /// assert!(tool.annotations.is_some());
103    /// ```
104    pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
105        self.annotations = Some(annotations);
106        self
107    }
108}
109
110/// Creates a new [`SdkMcpTool`] with the given name, description, schema, and handler.
111///
112/// This is the primary factory function for defining custom tools.
113///
114/// # Arguments
115///
116/// * `name` — Unique name for the tool.
117/// * `description` — What the tool does (shown to the model).
118/// * `input_schema` — JSON Schema for the tool's input parameters.
119/// * `handler` — Async function implementing the tool logic. Receives input as
120///   a JSON `Value` and should return a JSON `Value` in MCP result format.
121///
122/// # Example
123///
124/// ```rust,no_run
125/// # use claude_code::tool;
126/// # use serde_json::{json, Value};
127/// let my_tool = tool(
128///     "greet",
129///     "Greet someone by name",
130///     json!({"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}),
131///     |args: Value| async move {
132///         let name = args["name"].as_str().unwrap_or("world");
133///         Ok(json!({"content": [{"type": "text", "text": format!("Hello, {name}!")}]}))
134///     },
135/// );
136/// ```
137pub fn tool<F, Fut>(name: &str, description: &str, input_schema: Value, handler: F) -> SdkMcpTool
138where
139    F: Fn(Value) -> Fut + Send + Sync + 'static,
140    Fut: std::future::Future<Output = std::result::Result<Value, Error>> + Send + 'static,
141{
142    let wrapped: SdkMcpToolHandler = Arc::new(move |args: Value| Box::pin(handler(args)));
143    SdkMcpTool {
144        name: name.to_string(),
145        description: description.to_string(),
146        input_schema,
147        handler: wrapped,
148        annotations: None,
149    }
150}
151
152/// In-process MCP server that hosts custom tools.
153///
154/// Implements the MCP tool listing and calling protocol. Tool calls are dispatched
155/// to the registered handler functions and executed within your application.
156#[derive(Clone)]
157pub struct McpSdkServer {
158    /// Server name identifier.
159    pub name: String,
160    /// Server version string.
161    pub version: String,
162    tool_map: HashMap<String, SdkMcpTool>,
163}
164
165impl McpSdkServer {
166    /// Creates a new MCP server with the given name, version, and tools.
167    ///
168    /// # Example
169    ///
170    /// ```rust
171    /// use claude_code::{tool, McpSdkServer};
172    /// use serde_json::{json, Value};
173    ///
174    /// let tools = vec![tool(
175    ///     "ping",
176    ///     "Ping tool",
177    ///     json!({"type":"object"}),
178    ///     |_args: Value| async move { Ok(json!({"content": []})) },
179    /// )];
180    ///
181    /// let server = McpSdkServer::new("local-tools", "1.0.0", tools);
182    /// assert_eq!(server.name, "local-tools");
183    /// ```
184    pub fn new(
185        name: impl Into<String>,
186        version: impl Into<String>,
187        tools: Vec<SdkMcpTool>,
188    ) -> Self {
189        let mut tool_map = HashMap::new();
190        for tool in tools {
191            tool_map.insert(tool.name.clone(), tool);
192        }
193        Self {
194            name: name.into(),
195            version: version.into(),
196            tool_map,
197        }
198    }
199
200    /// Returns `true` if the server has any registered tools.
201    ///
202    /// # Example
203    ///
204    /// ```rust
205    /// use claude_code::McpSdkServer;
206    ///
207    /// let server = McpSdkServer::new("empty", "1.0.0", vec![]);
208    /// assert!(!server.has_tools());
209    /// ```
210    pub fn has_tools(&self) -> bool {
211        !self.tool_map.is_empty()
212    }
213
214    /// Returns JSON representations of all registered tools (for `tools/list` responses).
215    ///
216    /// # Example
217    ///
218    /// ```rust
219    /// use claude_code::{tool, McpSdkServer};
220    /// use serde_json::{json, Value};
221    ///
222    /// let server = McpSdkServer::new(
223    ///     "demo",
224    ///     "1.0.0",
225    ///     vec![tool(
226    ///         "echo",
227    ///         "Echo text",
228    ///         json!({"type":"object","properties":{"text":{"type":"string"}}}),
229    ///         |_args: Value| async move { Ok(json!({"content": []})) },
230    ///     )],
231    /// );
232    ///
233    /// let tools = server.list_tools_json();
234    /// assert_eq!(tools.len(), 1);
235    /// ```
236    pub fn list_tools_json(&self) -> Vec<Value> {
237        self.tool_map
238            .values()
239            .map(|tool| {
240                let mut base = json!({
241                    "name": tool.name,
242                    "description": tool.description,
243                    "inputSchema": tool.input_schema,
244                });
245                if let Some(annotations) = &tool.annotations
246                    && let Value::Object(ref mut obj) = base
247                {
248                    obj.insert(
249                        "annotations".to_string(),
250                        serde_json::to_value(annotations).unwrap_or(Value::Null),
251                    );
252                }
253                base
254            })
255            .collect()
256    }
257
258    /// Calls a tool by name with the given arguments and returns the JSON result.
259    ///
260    /// If the tool is not found or the handler returns an error, an error result
261    /// in MCP format is returned (with `isError: true`).
262    ///
263    /// # Example
264    ///
265    /// ```rust,no_run
266    /// use claude_code::{tool, McpSdkServer};
267    /// use serde_json::{json, Value};
268    ///
269    /// # async fn example() {
270    /// let server = McpSdkServer::new(
271    ///     "demo",
272    ///     "1.0.0",
273    ///     vec![tool(
274    ///         "echo",
275    ///         "Echo text",
276    ///         json!({"type":"object","properties":{"text":{"type":"string"}}}),
277    ///         |args: Value| async move {
278    ///             Ok(json!({"content":[{"type":"text","text":args["text"]}]}))
279    ///         },
280    ///     )],
281    /// );
282    ///
283    /// let result = server.call_tool_json("echo", json!({"text":"hello"})).await;
284    /// assert!(result.get("content").is_some());
285    /// # }
286    /// ```
287    pub async fn call_tool_json(&self, tool_name: &str, arguments: Value) -> Value {
288        let Some(tool) = self.tool_map.get(tool_name) else {
289            return json!({
290                "content": [
291                    {"type": "text", "text": format!("Tool '{tool_name}' not found")}
292                ],
293                "isError": true,
294                "is_error": true
295            });
296        };
297
298        match (tool.handler)(arguments).await {
299            Ok(result) => result,
300            Err(err) => json!({
301                "content": [{"type": "text", "text": err.to_string()}],
302                "isError": true,
303                "is_error": true
304            }),
305        }
306    }
307}
308
309/// Creates an [`McpSdkServerConfig`] for use in [`ClaudeAgentOptions::mcp_servers`](crate::ClaudeAgentOptions::mcp_servers).
310///
311/// This is the entry point for registering in-process MCP servers with the SDK.
312///
313/// # Arguments
314///
315/// * `name` — Unique server name.
316/// * `version` — Server version string.
317/// * `tools` — List of tools to register on this server.
318///
319/// # Returns
320///
321/// An [`McpSdkServerConfig`] that can be added to the `mcp_servers` map.
322///
323/// # Example
324///
325/// ```rust,no_run
326/// # use claude_code::{tool, create_sdk_mcp_server, ClaudeAgentOptions, McpServerConfig, McpServersOption};
327/// # use serde_json::{json, Value};
328/// # use std::collections::HashMap;
329/// let server = create_sdk_mcp_server("my-server", "1.0.0", vec![
330///     tool("hello", "Say hello", json!({"type": "object"}), |_| async { Ok(json!({"content": []})) }),
331/// ]);
332///
333/// let options = ClaudeAgentOptions {
334///     mcp_servers: McpServersOption::Servers(HashMap::from([
335///         ("my-server".to_string(), McpServerConfig::Sdk(server)),
336///     ])),
337///     ..Default::default()
338/// };
339/// ```
340pub fn create_sdk_mcp_server(
341    name: impl Into<String>,
342    version: impl Into<String>,
343    tools: Vec<SdkMcpTool>,
344) -> McpSdkServerConfig {
345    let name_string = name.into();
346    let version_string = version.into();
347    let server = Arc::new(McpSdkServer::new(
348        name_string.clone(),
349        version_string,
350        tools,
351    ));
352
353    McpSdkServerConfig {
354        type_: "sdk".to_string(),
355        name: name_string,
356        instance: server,
357    }
358}