Custom tools allow you to extend Claude Code's capabilities with your own functionality through in-process MCP servers, enabling Claude to interact with external services, APIs, or perform specialized operations.
Use the createSdkMcpServer and tool helper functions to define type-safe custom tools:
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk"; import { z } from "zod"; // Create an SDK MCP server with custom tools const customServer = createSdkMcpServer({ name: "my-custom-tools", version: "1.0.0", tools: [ tool( "get_weather", "Get current temperature for a location using coordinates", { latitude: z.number().describe("Latitude coordinate"), longitude: z.number().describe("Longitude coordinate") }, async (args) => { const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}¤t=temperature_2m&temperature_unit=fahrenheit`); const data = await response.json(); return { content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }] }; } ) ] });Pass the custom server to the query function via the mcpServers option as a dictionary/object.
Important: Custom MCP tools require streaming input mode. You must use an async generator/iterable for the prompt parameter - a simple string will not work with MCP servers.
When MCP tools are exposed to Claude, their names follow a specific format:
mcp__{server_name}__{tool_name}get_weather in server my-custom-tools becomes mcp__my-custom-tools__get_weatherYou can control which tools Claude can use via the allowedTools option:
import { query } from "@anthropic-ai/claude-agent-sdk"; // Use the custom tools in your query with streaming input async function* generateMessages() { yield { type: "user" as const, message: { role: "user" as const, content: "What's the weather in San Francisco?" } }; } for await (const message of query({ prompt: generateMessages(), // Use async generator for streaming input options: { mcpServers: { "my-custom-tools": customServer // Pass as object/dictionary, not array }, // Optionally specify which tools Claude can use allowedTools: [ "mcp__my-custom-tools__get_weather", // Allow the weather tool // Add other tools as needed ], maxTurns: 3 } })) { if (message.type === "result" && message.subtype === "success") { console.log(message.result); } }When your MCP server has multiple tools, you can selectively allow them:
const multiToolServer = createSdkMcpServer({ name: "utilities", version: "1.0.0", tools: [ tool("calculate", "Perform calculations", { /* ... */ }, async (args) => { /* ... */ }), tool("translate", "Translate text", { /* ... */ }, async (args) => { /* ... */ }), tool("search_web", "Search the web", { /* ... */ }, async (args) => { /* ... */ }) ] }); // Allow only specific tools with streaming input async function* generateMessages() { yield { type: "user" as const, message: { role: "user" as const, content: "Calculate 5 + 3 and translate 'hello' to Spanish" } }; } for await (const message of query({ prompt: generateMessages(), // Use async generator for streaming input options: { mcpServers: { utilities: multiToolServer }, allowedTools: [ "mcp__utilities__calculate", // Allow calculator "mcp__utilities__translate", // Allow translator // "mcp__utilities__search_web" is NOT allowed ] } })) { // Process messages }The @tool decorator supports various schema definition approaches for type safety:
import { z } from "zod"; tool( "process_data", "Process structured data with type safety", { // Zod schema defines both runtime validation and TypeScript types data: z.object({ name: z.string(), age: z.number().min(0).max(150), email: z.string().email(), preferences: z.array(z.string()).optional() }), format: z.enum(["json", "csv", "xml"]).default("json") }, async (args) => { // args is fully typed based on the schema // TypeScript knows: args.data.name is string, args.data.age is number, etc. console.log(`Processing ${args.data.name}'s data as ${args.format}`); // Your processing logic here return { content: [{ type: "text", text: `Processed data for ${args.data.name}` }] }; } )Handle errors gracefully to provide meaningful feedback:
tool( "fetch_data", "Fetch data from an API", { endpoint: z.string().url().describe("API endpoint URL") }, async (args) => { try { const response = await fetch(args.endpoint); if (!response.ok) { return { content: [{ type: "text", text: `API error: ${response.status} ${response.statusText}` }] }; } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Failed to fetch data: ${error.message}` }] }; } } )const databaseServer = createSdkMcpServer({ name: "database-tools", version: "1.0.0", tools: [ tool( "query_database", "Execute a database query", { query: z.string().describe("SQL query to execute"), params: z.array(z.any()).optional().describe("Query parameters") }, async (args) => { const results = await db.query(args.query, args.params || []); return { content: [{ type: "text", text: `Found ${results.length} rows:\n${JSON.stringify(results, null, 2)}` }] }; } ) ] });const apiGatewayServer = createSdkMcpServer({ name: "api-gateway", version: "1.0.0", tools: [ tool( "api_request", "Make authenticated API requests to external services", { service: z.enum(["stripe", "github", "openai", "slack"]).describe("Service to call"), endpoint: z.string().describe("API endpoint path"), method: z.enum(["GET", "POST", "PUT", "DELETE"]).describe("HTTP method"), body: z.record(z.any()).optional().describe("Request body"), query: z.record(z.string()).optional().describe("Query parameters") }, async (args) => { const config = { stripe: { baseUrl: "https://api.stripe.com/v1", key: process.env.STRIPE_KEY }, github: { baseUrl: "https://api.github.com", key: process.env.GITHUB_TOKEN }, openai: { baseUrl: "https://api.openai.com/v1", key: process.env.OPENAI_KEY }, slack: { baseUrl: "https://slack.com/api", key: process.env.SLACK_TOKEN } }; const { baseUrl, key } = config[args.service]; const url = new URL(`${baseUrl}${args.endpoint}`); if (args.query) { Object.entries(args.query).forEach(([k, v]) => url.searchParams.set(k, v)); } const response = await fetch(url, { method: args.method, headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" }, body: args.body ? JSON.stringify(args.body) : undefined }); const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } ) ] });const calculatorServer = createSdkMcpServer({ name: "calculator", version: "1.0.0", tools: [ tool( "calculate", "Perform mathematical calculations", { expression: z.string().describe("Mathematical expression to evaluate"), precision: z.number().optional().default(2).describe("Decimal precision") }, async (args) => { try { // Use a safe math evaluation library in production const result = eval(args.expression); // Example only! const formatted = Number(result).toFixed(args.precision); return { content: [{ type: "text", text: `${args.expression} = ${formatted}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: Invalid expression - ${error.message}` }] }; } } ), tool( "compound_interest", "Calculate compound interest for an investment", { principal: z.number().positive().describe("Initial investment amount"), rate: z.number().describe("Annual interest rate (as decimal, e.g., 0.05 for 5%)"), time: z.number().positive().describe("Investment period in years"), n: z.number().positive().default(12).describe("Compounding frequency per year") }, async (args) => { const amount = args.principal * Math.pow(1 + args.rate / args.n, args.n * args.time); const interest = amount - args.principal; return { content: [{ type: "text", text: `Investment Analysis:\n` + `Principal: $${args.principal.toFixed(2)}\n` + `Rate: ${(args.rate * 100).toFixed(2)}%\n` + `Time: ${args.time} years\n` + `Compounding: ${args.n} times per year\n\n` + `Final Amount: $${amount.toFixed(2)}\n` + `Interest Earned: $${interest.toFixed(2)}\n` + `Return: ${((interest / args.principal) * 100).toFixed(2)}%` }] }; } ) ] });