We use cookies to deliver and improve our services, analyze site usage, and if you agree, to customize or personalize your experience and market our services to you. You can read our Cookie Policy here.
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)}%`
}
]
};
}
)
]
});Was this page helpful?