Tools are capabilities the model can call while executing a task. The Qoder Agent SDK supports two kinds of tools:
With built-in tools you don't implement anything yourself—options control which tools this session can see, which are pre-approved, and which are denied.
Common built-in tools include
Define a custom tool when you want the model to call your own business capability, such as order lookup, internal knowledge base search, approval system calls, or read-only database access.
Custom tools usually involve three steps:
First, here is a complete minimal example. The following sections then explain each step.
This step defines the tool itself: its name, description, input parameters, execution logic, and metadata.
TypeScript's
Python tool handlers must be async functions, usually taking one
The two SDKs declare input schemas differently; both are normalized to MCP-protocol JSON Schema.
TypeScript: pass a Zod raw shape—a field object, not
Python: three styles are supported.
Style 1: a simple dict, for a handful of simple parameters. Keys are parameter names, values are Python types; every key is required in this style. Use
Style 2:
Style 3: a full JSON Schema dict, for enums, numeric ranges, string format constraints, or nested objects:
annotations carry MCP tool annotations. The SDK puts them on the MCP tool definition, and the CLI can use them for scheduling, permissions, or status display.
Common fields:
Note: these fields do not replace permission configuration. Whether a tool may run is still decided by
Python's
Once the server is in the MCP servers config, the CLI discovers its tools and calls back into your handler through the SDK when the model needs them.
The full custom tool name format is:
For example, with server name
When the model calls tools, the SDK provides multiple permission layers. You can decide:
These approaches compose. A common pattern: narrow the visible tool set with
When the same tool matches both allow and deny rules, the deny rule takes precedence.
The permission mode sets the whole session's default permission behavior with one line.
The permission callback (
Common return values:
When using custom tools in a subagent, use the full tool name as well:
If you already use the hooks system, use
Tool handlers have several error paths.
For expected business failures, return the error flag (
Good cases for the error flag:
If the handler throws, the MCP layer converts the exception into an error result, and the agent loop won't crash from an ordinary tool exception. But the model usually only sees the exception message—less controlled in format and content than an explicit error flag.
Recommendation: use the explicit error flag for expected business failures; throw only for truly unexpected exceptions.
The Python SDK performs a runtime fallback check on handler return values:
Tool handlers return an MCP
You can also return structured JSON strings, which help the model understand and continue processing:
Common content blocks:
The Python edition has two result differences worth noting:
- Built-in tools: Provided by Qoder CLI, such as reading files, searching, executing commands, and invoking subagents.
- Custom tools: defined by SDK users and exposed to the model as in-process MCP servers—TypeScript uses
tool()+createSdkMcpServer(), Python uses the@tool()decorator +create_sdk_mcp_server().
Built-in Tools
With built-in tools you don't implement anything yourself—options control which tools this session can see, which are pre-approved, and which are denied.
Read, Edit, Write, Bash, Glob, Grep, WebFetch, WebSearch, Agent, and more. Tool names are determined by the underlying Qoder CLI; permission configuration should use the tool names the CLI exposes to the model. See SDK References for the full list, names, and input/output structures.
Custom Tools
Define a custom tool when you want the model to call your own business capability, such as order lookup, internal knowledge base search, approval system calls, or read-only database access.
Custom tools usually involve three steps:
- Create the tool: TypeScript uses
tool(); Python decorates anasync defhandler with@tool(). - Register it on an in-process MCP server:
createSdkMcpServer()/create_sdk_mcp_server(). - Wire it up via the MCP servers config in options, and control invocation with permission settings.
Custom Tool Integration Steps
First, here is a complete minimal example. The following sections then explain each step.
Step 1: Create the tool
This step defines the tool itself: its name, description, input parameters, execution logic, and metadata.
Parameters
TypeScript's tool() takes 5 parameters; Python's @tool() decorator takes 4:
| Parameter | Required | Meaning |
|---|---|---|
name | Yes | Unique identifier of the tool within its MCP server |
description | Yes | Model-facing description: when to use the tool, what it does, what it returns |
inputSchema / input_schema | Yes | Tool input parameters: TypeScript takes a Zod raw shape; Python supports a simple dict, TypedDict, or a full JSON Schema dict |
handler (TypeScript) | Yes | Async function that receives parsed arguments and returns a CallToolResult; in Python the decorated async def is the handler |
extras.annotations (TypeScript) / annotations (Python) | No | MCP tool annotations such as readOnlyHint, destructiveHint, openWorldHint |
args dict. If the handler declares a second positional parameter, the SDK passes a ToolInvocationContext whose signal is an asyncio.Event set when the CLI cancels the in-flight tool call—useful for long tasks to stop proactively:
Configure Input Parameters
The two SDKs declare input schemas differently; both are normalized to MCP-protocol JSON Schema.
TypeScript: pass a Zod raw shape—a field object, not z.object(...):
| Need | TypeScript syntax |
|---|---|
| Required string | z.string().describe('...') |
| Optional parameter | z.string().optional().describe('...') |
| Default value | z.number().default(5) |
| Enum | z.enum(['docs', 'tickets']) |
| Numeric range | z.number().min(1).max(10) |
typing.Annotated to attach descriptions:
| Python form | JSON Schema meaning |
|---|---|
str | {"type": "string"} |
int | {"type": "integer"} |
float | {"type": "number"} |
bool | {"type": "boolean"} |
list[str] | String array |
dict | Object |
Annotated[T, "..."] | Adds description to T's schema |
TypedDict, for many fields, optional fields, or reusable type definitions. Mark optional fields with NotRequired (importable from typing on Python 3.11+; use typing_extensions on 3.10):
Configure Tool Metadata
annotations carry MCP tool annotations. The SDK puts them on the MCP tool definition, and the CLI can use them for scheduling, permissions, or status display.
| Field | Type | Meaning |
|---|---|---|
title | string | Human-readable tool title |
readOnlyHint | boolean | Marks the tool as read-only, changing no state |
destructiveHint | boolean | Marks the tool as potentially modifying or deleting data |
openWorldHint | boolean | Marks the tool as reaching external systems or the network |
maxResultSizeChars (Python) | int | Passed to the CLI via _meta["anthropic/maxResultSizeChars"] to relax the tool output length limit |
tools, allow/deny rules, the permission mode, the permission callback, and hooks. The annotations echoed in MCP status may also use the CLI-projected names readOnly, destructive, openWorld instead of the raw MCP *Hint names; keep a mapping on your side if the host UI needs them.
Step 2: Register with an MCP Server
createSdkMcpServer() / create_sdk_mcp_server() registers one or more tools as a same-process MCP server. The server name becomes part of the full tool name, so keep it short and stable.
| Field | How to set it | Description |
|---|---|---|
name | For example kb, orders | Server name; forms full tool names like mcp__{name}__{tool} |
version | For example '1.0.0' | Informational version, optional |
tools | [searchDocs, lookupOrder] | Tools registered to this server |
create_sdk_mcp_server() validates synchronously: server name / tool names / tool descriptions must be non-empty strings, and tool names must be unique within a server.
Step 3: Attach to query()
Once the server is in the MCP servers config, the CLI discovers its tools and calls back into your handler through the SDK when the model needs them.
orders and tool name lookup_order, the full tool name is mcp__orders__lookup_order. This full name is used in tool allowlists/denylists, permission callbacks, hooks matchers, and subagent tools configuration.
Python's QoderSDKClient multi-turn sessions use the same mcp_servers configuration:
Controlling Tool Permissions
When the model calls tools, the SDK provides multiple permission layers. You can decide:
- Which tools are provided to the current session.
- Which tools are allowed by default.
- Which tools are explicitly denied.
- Whether the host application should make a dynamic decision before each tool call.
Permission Control Overview
| Method (TypeScript / Python) | Effect | Granularity | When to use |
|---|---|---|---|
tools | Limits the visible tool set for this session | Session | Narrow the tools the model can see at the source |
allowedTools / allowed_tools, disallowedTools / disallowed_tools | Pre-approve or deny specific tools | Per tool | You know exactly which tools to allow or deny |
permissionMode / permission_mode | Set the session's default permission policy | Global | Quickly switch plan mode, auto-accept edits, bypass, etc. |
canUseTool / can_use_tool | Run custom logic before every invocation | Per call | Decisions depend on argument contents |
hooks.PreToolUse | Intercepts tool calls through the hooks lifecycle | Call | You already use hooks and want shared auditing or blocking |
tools, set static rules with allow/deny, then use the permission callback for argument-level decisions.
Approach 1: tool set plus allow/deny rules
tools controls the session's visible tool set; allow/deny rules control permissions. Custom MCP tools must use full tool names.
Approach 2: permission mode
The permission mode sets the whole session's default permission behavior with one line.
| Mode | Effect |
|---|---|
default | Standard permission behavior; sensitive operations follow rules or runtime policy |
acceptEdits | Auto-accepts file edits; other sensitive operations still follow the permission policy |
bypassPermissions | Skips permission checks; requires the explicit skip confirmation as well |
yolo | Compatibility alias of bypassPermissions; also requires explicit confirmation |
plan | Plan mode, for having the model produce a proposal first |
dontAsk | No interactive prompts; operations not pre-approved or allowed by rules are denied |
auto | The runtime decides allow or deny automatically |
Approach 3: permission callback
The permission callback (canUseTool / can_use_tool) runs before each tool call. Return allow or deny based on the tool name, argument contents, and approval context.
| Return | Effect |
|---|---|
| allow | Executes with the original arguments |
| allow + updated input | Executes with replaced tool arguments |
deny + message | Rejects; the model sees the reason and can try another way |
deny + message + interrupt: true | Rejects and interrupts the current agent loop |
Method 4: hooks.PreToolUse
If you already use the hooks system, use PreToolUse to intercept or audit tool calls in one place.
PreToolUse's permissionDecision can be allow, deny, ask, or defer. See Permissions for the full permission strategy.
How the SDK Handles Tool Errors
Tool handlers have several error paths.
Business failures: return an explicit error flag
For expected business failures, return the error flag (isError: true in TypeScript, is_error: True in Python). The SDK converts the result into an MCP CallToolResult for the CLI; the model sees the failure content and may retry or choose another approach.
- Arguments are valid, but no business result exists, such as an order not found.
- A security policy rejects execution, such as only allowing
SELECTqueries. - An external service returns a business error that can be explained.
Unexpected Exception: Handler Throws
If the handler throws, the MCP layer converts the exception into an error result, and the agent loop won't crash from an ordinary tool exception. But the model usually only sees the exception message—less controlled in format and content than an explicit error flag.
Malformed returns: wrapped as errors by the SDK (Python)
The Python SDK performs a runtime fallback check on handler return values:
- Returning
None: converted to error text explaining that the handler must return a dict containing"content". - Returning a non-dict (e.g., string, number, list): converted to text content and marked
isError=True. - Returning a dict but without
"content": converted to error text and lists the actual keys. - Returning unsupported content types: that content block is skipped and a warning is logged.
Tool Return Values
Tool handlers return an MCP CallToolResult (a dict in Python, converted by the SDK). Text content is the most common:
| Type | Shape | Description |
|---|---|---|
| Text | { type: 'text', text } | Most common; suitable for natural language or JSON strings |
| Image | { type: 'image', data, mimeType } | data is base64 |
| Audio | { type: 'audio', data, mimeType } | data is base64 (TypeScript) |
| Resource link | { type: 'resource_link', uri, name?, description?, mimeType? } | Returns a referenceable resource; Python degrades it to text, concatenating name / uri / description for the model |
| Embedded resource | { type: 'resource', resource } | Returns text or binary resource content; Python converts embedded text resources to TextContent |
- The handler-returned dict's top-level
_metais not propagated toCallToolResult. - When the handler indicates an error, use the Python field name
"is_error": True, not the MCP/TypeScript-styleisError. The SDK maps it to the MCP result internally.
Common Pitfalls
- When writing permission configuration for custom tools, use the full
mcp__server__toolname. - In TypeScript,
tool()'s third parameter takes a Zod raw shape—do not passz.object(...). - All fields in a Python simple-dict schema are required; use
TypedDict + NotRequiredor a full JSON Schema for optional fields. - For enums, numeric ranges, nested objects, or string pattern/format constraints, use a full JSON Schema dict in Python.
- Python handlers must be
async defand return a dict containing a"content"list. - Tool descriptions should state "when to use it, what it does, what it returns"—never just
queryorhelper. readOnlyHintis tool metadata and a scheduling hint, not a permission switch. Whether execution is allowed is still determined by permission configuration.- Avoid putting a huge all-purpose business entry point into one universal tool. A tool should complete one clear class of action.
Continue Reading
- Tools Reference: built-in tool list, tool creation APIs,
CallToolResult, built-in tool I/O types. - MCP Integration: in-process, stdio, SSE, HTTP, OAuth, and other MCP server integration methods.
- Permissions: permission modes, tool allowlists, permission callbacks, permission rule updates.
- Subagent Guide: Let different agents use different tool sets.