Skip to main content
The Claude Code SDK lets you extend the agent with your own tools via the Model Context Protocol (MCP). Rather than running a separate MCP server process, you can define tools in TypeScript and register them as an in-process MCP server. The agent can call these tools just like any built-in tool — with full permission handling and streaming support.

tool()

Defines a single custom MCP tool.
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>
name
string
required
Tool name as it appears to the model. Use snake_case to match MCP conventions (e.g. 'get_weather', 'run_tests').
description
string
required
Natural language description of what the tool does. The model uses this description to decide when and how to call the tool — write it clearly and include any important caveats or limitations.
inputSchema
AnyZodRawShape
required
A Zod object shape describing the tool’s input parameters. Use z.object({ ... }) fields directly (pass the shape, not the wrapped schema).
import { z } from 'zod'

// Pass the shape object, not z.object(shape)
const schema = {
  query: z.string().describe('The search query'),
  limit: z.number().optional().describe('Maximum results to return'),
}
handler
(args: InferShape<Schema>, extra: unknown) => Promise<CallToolResult>
required
Async function that executes the tool. Receives the validated input arguments typed according to the Zod schema.Return a CallToolResult from @modelcontextprotocol/sdk/types.js:
{ content: [{ type: 'text', text: 'result string' }] }
// or for errors:
{ content: [{ type: 'text', text: 'error message' }], isError: true }
extras
object
Optional metadata for the tool.
Returns: SdkMcpToolDefinition<Schema> — an opaque tool definition object to pass to createSdkMcpServer().

createSdkMcpServer()

Creates an in-process MCP server from a set of tool definitions. The server runs within the same Node.js process as the SDK consumer — no subprocess, no stdio transport, no network overhead.
If your SDK MCP tool handlers run longer than 60 seconds, set the CLAUDE_CODE_STREAM_CLOSE_TIMEOUT environment variable to a higher value.
function createSdkMcpServer(options: {
  name: string
  version?: string
  tools?: Array<SdkMcpToolDefinition<any>>
}): McpSdkServerConfigWithInstance
name
string
required
Name for the MCP server. Visible in server status displays.
version
string
Optional version string for the server (e.g. '1.0.0').
tools
Array<SdkMcpToolDefinition<any>>
Array of tool definitions created with tool(). The agent will have access to all listed tools.
Returns: McpSdkServerConfigWithInstance — a server config object to pass to the sdkMcpServers option of query() or SDKSessionOptions.

Registering tools with query()

Pass the server config in the sdkMcpServers option:
import { query, tool, createSdkMcpServer } from '@anthropic-ai/claude-code'
import { z } from 'zod'

const myTool = tool(
  'get_file_stats',
  'Returns the size and line count of a file.',
  {
    filePath: z.string().describe('Absolute path to the file'),
  },
  async ({ filePath }) => {
    const { statSync } = await import('fs')
    const { execSync } = await import('child_process')
    const stats = statSync(filePath)
    const lines = execSync(`wc -l < "${filePath}"`).toString().trim()
    return {
      content: [{ type: 'text', text: `Size: ${stats.size} bytes, Lines: ${lines}` }],
    }
  },
)

const server = createSdkMcpServer({
  name: 'my-tools',
  version: '1.0.0',
  tools: [myTool],
})

const stream = query({
  prompt: 'How many lines are in src/index.ts?',
  options: {
    cwd: '/path/to/project',
    sdkMcpServers: [server],
    model: 'claude-opus-4-5',
  },
})

for await (const message of stream) {
  if (message.type === 'result') {
    console.log(message.result)
  }
}

Complete example

The following example defines a custom database-query tool and a schema-inspection tool, registers them together, and runs an agent that can use both:
import { query, tool, createSdkMcpServer } from '@anthropic-ai/claude-code'
import { z } from 'zod'
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'

// --- Tool 1: execute a read-only SQL query ---
const sqlQueryTool = tool(
  'sql_query',
  'Executes a read-only SQL SELECT query against the application database and returns the results as JSON.',
  {
    query: z.string().describe('The SQL SELECT statement to execute'),
    limit: z
      .number()
      .int()
      .min(1)
      .max(1000)
      .optional()
      .describe('Maximum number of rows to return (default: 100)'),
  },
  async ({ query: sql, limit = 100 }): Promise<CallToolResult> => {
    // Replace with your actual database client
    try {
      const rows = await fakeDatabaseQuery(sql, limit)
      return {
        content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }],
      }
    } catch (err) {
      return {
        content: [{ type: 'text', text: `Query error: ${err}` }],
        isError: true,
      }
    }
  },
  {
    annotations: { readOnly: true },
  },
)

// --- Tool 2: list available tables ---
const listTablesTool = tool(
  'list_tables',
  'Returns the names and row counts of all tables in the database.',
  {},
  async (): Promise<CallToolResult> => {
    const tables = await fakeListTables()
    return {
      content: [{ type: 'text', text: JSON.stringify(tables, null, 2) }],
    }
  },
  {
    annotations: { readOnly: true },
    alwaysLoad: true,
  },
)

// --- Create the in-process MCP server ---
const dbServer = createSdkMcpServer({
  name: 'database-tools',
  version: '1.0.0',
  tools: [sqlQueryTool, listTablesTool],
})

// --- Run the agent with the custom tools ---
async function main() {
  const stream = query({
    prompt: 'How many users signed up in the last 7 days? Show me the data.',
    options: {
      model: 'claude-opus-4-5',
      sdkMcpServers: [dbServer],
      permissionMode: 'acceptEdits',
      maxTurns: 5,
    },
  })

  for await (const message of stream) {
    if (message.type === 'assistant') {
      for (const block of message.message.content) {
        if (block.type === 'text') process.stdout.write(block.text)
      }
    }
    if (message.type === 'result') {
      console.log(`\n\nDone in ${message.num_turns} turns.`)
    }
  }
}

main().catch(console.error)

// Stubs — replace with real implementations
async function fakeDatabaseQuery(sql: string, limit: number) {
  return [{ user_id: 1, signup_date: '2026-03-25' }]
}
async function fakeListTables() {
  return [{ name: 'users', row_count: 1024 }]
}