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.
Parameters
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').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…”.
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.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: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.
Additional context hint for tool search and discovery.
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
createSdkMcpServer()
Bundles one or more SdkMcpToolDefinition objects into an in-process MCP server that the SDK can attach to a query() call.
Parameters
Server name. Used internally to identify the server within the session. Must be unique among all MCP servers attached to the same
query() call.Server version string (e.g.
'1.0.0'). Reported in the MCP server info. Defaults to '1.0.0'.Array of tool definitions created with
tool(). These are the tools the server exposes to Claude.Return value
Returns aMcpSdkServerConfigWithInstance — an opaque config object with type: 'sdk' that you pass directly to the mcpServers option of query().
Example
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 toquery().
Tool design tips
Write descriptions from Claude's perspective
Write descriptions from Claude's perspective
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.
Use Zod's .describe() on every field
Use Zod's .describe() on every field
Field descriptions are included in the schema Claude sees. They are your primary way to convey constraints, formats, and semantics for each parameter.
Return errors via isError, not exceptions
Return errors via isError, not exceptions
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.Annotate side effects accurately
Annotate side effects accurately
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.Keep long-running tools within the timeout
Keep long-running tools within the timeout
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.Using custom tools alongside external MCP servers
In-process servers and external (stdio/SSE/HTTP) MCP servers can coexist in the samequery() call. Pass all of them together in the mcpServers map: