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}