Skip to main content
The SDK lets you give Claude new capabilities by defining custom tools that run directly in your Node.js process. You define each tool with a Zod schema and a handler function, bundle them into an in-process MCP server with createSdkMcpServer(), and pass the server to query() via the mcpServers option. Because the tools run in the same process as your application, they can access databases, call internal APIs, read private configuration, and use any Node.js library — without needing to spawn a separate subprocess.

tool()

Defines a single custom tool.
import { tool } from '@anthropic-ai/claude-code'
import type { SdkMcpToolDefinition } from '@anthropic-ai/claude-code'
import { z } from 'zod'

function tool<Schema extends AnyZodRawShape>(
  name: string,
  description: string,
  inputSchema: Schema,
  handler: (args: InferShape<Schema>, extra: unknown) => Promise<CallToolResult>,
  extras?: {
    annotations?: ToolAnnotations
    searchHint?: string
    alwaysLoad?: boolean
  },
): SdkMcpToolDefinition<Schema>

Parameters

name
string
required
Tool name. This is the identifier Claude uses when deciding to call the tool. Use a clear, action-oriented name (e.g. 'lookup_customer', 'run_query').
description
string
required
Natural language description of what the tool does and when to use it. This is the primary signal Claude uses to select the tool. Write it from Claude’s perspective: “Use this tool to…”.
inputSchema
ZodRawShape
required
A Zod object shape (the argument to z.object(...), not the schema itself) that defines the tool’s input parameters. The SDK infers the TypeScript type for the handler’s args parameter from this schema.
// Correct — pass the shape, not z.object(shape)
{ customerId: z.string(), includeOrders: z.boolean().optional() }
handler
(args, extra) => Promise<CallToolResult>
required
The function invoked when Claude calls the tool. Receives the validated arguments (typed from the schema) and an opaque extra context. Must return a CallToolResult from @modelcontextprotocol/sdk/types.js.A CallToolResult is:
{
  content: Array<TextContent | ImageContent | EmbeddedResource>
  isError?: boolean
}
extras.annotations
ToolAnnotations
MCP tool annotations. Relevant fields:
  • readOnly: boolean — Tool does not modify state.
  • destructive: boolean — Tool may cause irreversible changes (triggers permission prompts by default).
  • openWorld: boolean — Tool accesses external systems.
extras.searchHint
string
Additional context hint for tool search and discovery.
extras.alwaysLoad
boolean
When true, this tool is always included in the context even when tool search would otherwise omit it.

Return value

tool() returns an opaque SdkMcpToolDefinition handle. Pass it to createSdkMcpServer().

Example

import { tool } from '@anthropic-ai/claude-code'
import { z } from 'zod'
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'

const lookupCustomer = tool(
  'lookup_customer',
  'Use this tool to fetch a customer record from the database by email address.',
  {
    email: z.string().email().describe('Customer email address'),
  },
  async ({ email }): Promise<CallToolResult> => {
    const customer = await db.customers.findByEmail(email)
    if (!customer) {
      return {
        content: [{ type: 'text', text: `No customer found for ${email}` }],
        isError: true,
      }
    }
    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(customer, null, 2),
        },
      ],
    }
  },
)

createSdkMcpServer()

Bundles one or more SdkMcpToolDefinition objects into an in-process MCP server that the SDK can attach to a query() call.
import { createSdkMcpServer } from '@anthropic-ai/claude-code'
import type { McpSdkServerConfigWithInstance } from '@anthropic-ai/claude-code'

function createSdkMcpServer(options: {
  name: string
  version?: string
  tools?: Array<SdkMcpToolDefinition<any>>
}): McpSdkServerConfigWithInstance

Parameters

name
string
required
Server name. Used internally to identify the server within the session. Must be unique among all MCP servers attached to the same query() call.
version
string
Server version string (e.g. '1.0.0'). Reported in the MCP server info. Defaults to '1.0.0'.
tools
SdkMcpToolDefinition[]
Array of tool definitions created with tool(). These are the tools the server exposes to Claude.

Return value

Returns a McpSdkServerConfigWithInstance — an opaque config object with type: 'sdk' that you pass directly to the mcpServers option of query().

Example

import { createSdkMcpServer, query } from '@anthropic-ai/claude-code'
import { lookupCustomer, updateCustomer } from './myTools.js'

const server = createSdkMcpServer({
  name: 'customer-db',
  version: '1.0.0',
  tools: [lookupCustomer, updateCustomer],
})

for await (const message of query({
  prompt: 'Find customer [email protected] and update her plan to "pro".',
  options: {
    mcpServers: { 'customer-db': server },
    permissionMode: 'acceptEdits',
  },
})) {
  if (message.type === 'result' && message.subtype === 'success') {
    console.log(message.result)
  }
}

Complete example

The following example defines two tools — a read-only database query tool and a write tool — bundles them in an in-process server, and passes it to query().
import { tool, createSdkMcpServer, query } from '@anthropic-ai/claude-code'
import { z } from 'zod'
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
import Database from 'better-sqlite3'

const db = new Database('./app.db')

// ── Read tool ──────────────────────────────────────────────────────────────

const runQuery = tool(
  'run_query',
  'Use this tool to execute a read-only SQL SELECT query against the application database.',
  {
    sql: z.string().describe('The SQL SELECT statement to run'),
  },
  async ({ sql }): Promise<CallToolResult> => {
    if (!/^\s*SELECT/i.test(sql)) {
      return {
        content: [{ type: 'text', text: 'Only SELECT statements are allowed.' }],
        isError: true,
      }
    }
    try {
      const rows = db.prepare(sql).all()
      return {
        content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }],
      }
    } catch (err) {
      return {
        content: [{ type: 'text', text: String(err) }],
        isError: true,
      }
    }
  },
  {
    annotations: { readOnly: true },
  },
)

// ── Write tool ─────────────────────────────────────────────────────────────

const insertRow = tool(
  'insert_row',
  'Use this tool to insert a new row into the specified table.',
  {
    table: z.string().describe('Table name'),
    data: z.record(z.string(), z.unknown()).describe('Column-value pairs to insert'),
  },
  async ({ table, data }): Promise<CallToolResult> => {
    const cols = Object.keys(data).join(', ')
    const placeholders = Object.keys(data).map(() => '?').join(', ')
    const values = Object.values(data)
    try {
      db.prepare(`INSERT INTO ${table} (${cols}) VALUES (${placeholders})`).run(values)
      return { content: [{ type: 'text', text: 'Row inserted.' }] }
    } catch (err) {
      return { content: [{ type: 'text', text: String(err) }], isError: true }
    }
  },
  {
    annotations: { destructive: true },
  },
)

// ── Wire up and run ────────────────────────────────────────────────────────

const dbServer = createSdkMcpServer({
  name: 'app-database',
  tools: [runQuery, insertRow],
})

for await (const message of query({
  prompt: 'How many users signed up last week? Then create a summary row in the reports table.',
  options: {
    model: 'claude-opus-4-5',
    mcpServers: { 'app-database': dbServer },
    permissionMode: 'acceptEdits',
  },
})) {
  if (message.type === 'assistant') {
    for (const block of message.message.content) {
      if (block.type === 'text') process.stdout.write(block.text)
    }
  }
  if (message.type === 'result') break
}

Tool design tips

Descriptions drive tool selection. Start with “Use this tool to…” and be explicit about when the tool is appropriate. A vague description leads to missed calls or hallucinated usage.
Field descriptions are included in the schema Claude sees. They are your primary way to convey constraints, formats, and semantics for each parameter.
{
  userId: z.string().uuid().describe('User UUID from the accounts table'),
  status: z.enum(['active', 'suspended', 'deleted']).describe('New account status'),
}
Throwing an exception from a handler is a fatal error. Return { content: [...], isError: true } for expected failure cases (record not found, validation failed) so Claude can recover gracefully.
Set annotations.readOnly: true for queries that only read state. Set annotations.destructive: true for operations that delete or overwrite data. Claude Code uses these annotations to decide whether to surface a permission prompt.
By default, the SDK stream closes after 60 seconds of inactivity. If your tool may take longer, set the environment variable CLAUDE_CODE_STREAM_CLOSE_TIMEOUT to a larger value (in milliseconds) before starting the process.
CLAUDE_CODE_STREAM_CLOSE_TIMEOUT=300000 node my-agent.js

Using custom tools alongside external MCP servers

In-process servers and external (stdio/SSE/HTTP) MCP servers can coexist in the same query() call. Pass all of them together in the mcpServers map:
const server = createSdkMcpServer({
  name: 'internal-tools',
  tools: [myTool],
})

for await (const message of query({
  prompt: 'Use the internal tool and also search the web.',
  options: {
    mcpServers: {
      // in-process SDK server
      'internal-tools': server,
      // external stdio server
      'web-search': {
        type: 'stdio',
        command: 'mcp-server-fetch',
        args: [],
      },
    },
  },
})) {
  // ...
}

Build docs developers (and LLMs) love