query, and control which tools Claude can access. It also covers error handling, tool annotations, and returning non-text content like images.
Quick reference
Create a custom tool
A tool is defined by four parts, passed as arguments to thetool() helper in TypeScript or the @tool decorator in Python:
- Name: a unique identifier Claude uses to call the tool.
- Description: what the tool does. Claude reads this to decide when to call it.
- Input schema: the arguments Claude must provide. In TypeScript this is always a Zod schema, and the handler’s
argsare typed from it automatically. In Python this is a dict mapping names to types, like{"latitude": float}, which the SDK converts to JSON Schema for you. The Python decorator also accepts a full JSON Schema dict directly when you need enums, ranges, optional fields, or nested objects. - Handler: the async function that runs when Claude calls the tool. It receives the validated arguments and must return an object with:
content(required): an array of result blocks, each with atypeof"text","image","audio","resource", or"resource_link". See Return images and resources for non-text blocks.structuredContent(optional): a JSON object holding the result as machine-readable data, returned alongsidecontent. See Return structured data.isError(optional): set totrueto signal a tool failure so Claude can react to it. See Handle errors.
createSdkMcpServer (TypeScript) or create_sdk_mcp_server (Python). The server runs in-process inside your application, not as a separate process.
Weather tool example
This example defines aget_temperature tool and wraps it in an MCP server. It only sets up the tool; to pass it to query and run it, see Call a custom tool below.
tool() TypeScript reference or the @tool Python reference for full parameter details, including JSON Schema input formats and return value structure.
Call a custom tool
Pass the MCP server you created toquery via the mcpServers option. The key in mcpServers becomes the {server_name} segment in each tool’s fully qualified name: mcp__{server_name}__{tool_name}. List that name in allowedTools so the tool runs without a permission prompt.
These snippets reuse the weatherServer from the example above to ask Claude what the weather is in a specific location.
Add more tools
A server holds as many tools as you list in itstools array. With more than one tool on a server, you can list each one in allowedTools individually or use the wildcard mcp__weather__* to cover every tool the server exposes.
The example below adds a second tool, get_precipitation_chance, to the weatherServer from the weather tool example and rebuilds it with both tools in the array.
Add tool annotations
Tool annotations are optional metadata describing how a tool behaves. Pass them as the fifth argument totool() helper in TypeScript or via the annotations keyword argument for the @tool decorator in Python. All hint fields are Booleans.
Annotations are metadata, not enforcement. A tool marked
readOnlyHint: true can still write to disk if that’s what the handler does. Keep the annotation accurate to the handler.
This example adds readOnlyHint to the get_temperature tool from the weather tool example.
ToolAnnotations in the TypeScript or Python reference.
Control tool access
The weather tool example registered a server and listed tools inallowedTools. This section covers how tool names are constructed and how to scope access when you have multiple tools or want to restrict built-ins.
Tool name format
When MCP tools are exposed to Claude, their names follow a specific format:- Pattern:
mcp__{server_name}__{tool_name} - Example: A tool named
get_temperaturein serverweatherbecomesmcp__weather__get_temperature
Configure allowed tools
Thetools option and the allowed/disallowed lists affect two layers: availability, which controls whether a tool appears in Claude’s context, and permission, which controls whether a call is approved once Claude attempts it. tools and bare-name disallowedTools entries change availability. allowedTools and scoped disallowedTools rules change permission only.
To remove a built-in entirely, omit it from
tools or list its bare name in disallowedTools (Python: disallowed_tools); both keep the tool out of context so Claude never attempts it. A scoped disallowedTools rule blocks matching calls but leaves the tool visible, so Claude may waste a turn trying it. See Configure permissions for the full evaluation order.
Handle errors
A handler error doesn’t stop the agent loop. The SDK’s in-process MCP server catches uncaught exceptions and returns them as error results, so how you report an error determines what Claude reads, not whether the query fails:
In both cases Claude can retry, try a different tool, or explain the failure. Catch errors yourself when the raw exception message isn’t enough for Claude to act on.
The example below catches two kinds of failures inside the handler and composes the error message Claude reads. A non-200 HTTP status is caught from the response and returned as an error result. A network error or invalid JSON is caught by the surrounding
try/except (Python) or try/catch (TypeScript) and also returned as an error result. In both cases Claude receives a message that describes the failure instead of a bare exception string.
Return images and resources
Thecontent array in a tool result accepts text, image, audio, resource, and resource_link blocks. You can mix them in the same response. In TypeScript, audio blocks are saved to disk and Claude receives a text block with the saved file path; in Python, the SDK drops audio blocks from the tool result and logs a warning. Resource link blocks are converted to a text block containing the link’s name, URI, and description.
Images
An image block carries the image bytes inline, encoded as base64. There is no URL field. To return an image that lives at a URL, fetch it in the handler, read the response bytes, and base64-encode them before returning. The result is processed as visual input.Resources
A resource block embeds a piece of content identified by a URI. The URI is a label for Claude to reference; the actual content rides in the block’stext or blob field. Use this when your tool produces something that makes sense to address by name later, such as a generated file or a record from an external system.
This example shows a resource block returned from inside a tool handler. The URI
file:///tmp/report.md is a label that Claude can reference later; the SDK does not read from that path.
CallToolResult type. See the MCP specification for the full definition.
Return structured data
structuredContent is an optional JSON object on the result, separate from the content array. Use it to return raw values that Claude can read as exact fields instead of parsing them out of a text string or image.
When structuredContent is set, Claude receives the JSON plus any image or resource blocks from content. Text blocks in content are not forwarded, since they are assumed to duplicate the structured data. The example below renders a chart as an image block and returns the data points behind it in structuredContent from the same handler. In the snippet, chartPngBuffer is a Buffer holding the rendered PNG bytes.
TypeScript
The Python
@tool decorator forwards only content and is_error from the handler’s return dict. To return structuredContent from Python, run a standalone MCP server instead of an in-process SDK server.Example: unit converter
This tool converts values between units of length, temperature, and weight. A user can ask “convert 100 kilometers to miles” or “what is 72°F in Celsius,” and Claude picks the right unit type and units from the request. It demonstrates two patterns:- Enum schemas:
unit_typeis constrained to a fixed set of values. In TypeScript, usez.enum(). In Python, the dict schema doesn’t support enums, so the full JSON Schema dict is required. - Unsupported input handling: when a conversion pair isn’t found, the handler returns
isError: trueso Claude can tell the user what went wrong rather than treating a failure as a normal result.
query the same way as the weather example. This example sends three different prompts in a loop to show the same tool handling different unit types. For each response, it inspects AssistantMessage objects (which contain the tool calls Claude made during that turn) and prints each ToolUseBlock before printing the final ResultMessage text. This lets you see when Claude is using the tool versus answering from its own knowledge.
Next steps
Custom tools wrap async functions in a standard interface. You can mix the patterns on this page in the same server: a single server can hold a database tool, an API gateway tool, and an image renderer alongside each other. From here:- If your server grows to dozens of tools, see tool search to defer loading them until Claude needs them.
- To connect to external MCP servers (filesystem, GitHub, Slack) instead of building your own, see Connect MCP servers.
- To control which tools run automatically versus requiring approval, see Configure permissions.