- Built-in tools: Provided by Qoder CLI, such as reading files, searching, executing commands, and invoking subagents.
- Custom tools: Defined by SDK users through
@tool()andcreate_sdk_mcp_server()as Python functions and exposed to the model via an in-process MCP server.
Built-in Tools
When using built-in tools, you do not implement the tools yourself. You only control which tools the current session sees, which tools are pre-authorized, and which tools are denied throughQoderAgentOptions.
Read, Edit, Write, Bash, Glob, Grep, WebFetch, WebSearch, and Agent. Tool names are determined by the underlying Qoder CLI; in permission configuration, use the names the CLI exposes to the model — for example Read, Write, Bash. For the complete built-in tool list, see Tools Reference - Built-in Tool List.
Custom Tools
Define a custom tool when you want the model to call your own business capability — for example, querying orders, searching an internal knowledge base, calling an approval system, or accessing a read-only database. Python custom tools usually involve three steps:- Decorate an
async defhandler with@tool(). - Register one or more tools to an in-process MCP server with
create_sdk_mcp_server(). - Attach via
mcp_serversinQoderAgentOptions, and control calls with permission settings.
Custom Tool Integration Steps
First, here is a complete minimal example. The following sections then explain what each of the three steps can configure:Step 1: Create a Tool with @tool()
This step defines the tool itself: its name, description, input parameters, execution logic, and metadata.
@tool() Arguments
@tool() is a decorator. It has 4 arguments:
| Argument | Type | Required | Meaning |
|---|---|---|---|
name | str | Yes | Unique tool identifier within the current MCP server |
description | str | Yes | Tool description for the model; explain when to use it, what it does, and what it returns |
input_schema | type | dict[str, Any] | Yes | Defines tool input parameters; supports simple dict, TypedDict, and full JSON Schema dict |
annotations | ToolAnnotations | None | No | MCP tool annotations such as readOnlyHint, destructiveHint, openWorldHint |
SdkMcpTool type, see Tools Reference - tool().
The tool handler must be an async function and typically receives an args dict:
ToolInvocationContext. Its signal is an asyncio.Event that is set when the CLI cancels the current tool call — useful for long tasks to bail out cooperatively:
Configure Input Parameters
The Python edition’sinput_schema supports three forms. They are all normalized to MCP’s JSON Schema.
Form 1: simple dict
Suitable for a few simple parameters. Dict keys are parameter names and values are Python types; with this form, all keys are required.
typing.Annotated to attach descriptions to fields:
| 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
Suitable when there are many fields, optional fields are needed, or you want to reuse type definitions. Use NotRequired for optional fields:
NotRequired directly from typing; Python 3.10 needs to import it from typing_extensions.
Form 3: full JSON Schema dict
Use a full JSON Schema when you need enums, numeric ranges, string format constraints, or nested objects:
TypedDict + NotRequired or the required list of a full JSON Schema.
Configure Tool Metadata
annotations uses mcp.types.ToolAnnotations. The SDK puts it on the MCP tool definition; the CLI can use it for scheduling, permissions, or status display.
| Field | Type | Meaning |
|---|---|---|
title | str | Human-readable title for the tool |
readOnlyHint | bool | Marks the tool as read-only and not modifying any state |
destructiveHint | bool | Marks that the tool may modify or delete data |
openWorldHint | bool | Marks that the tool accesses external systems or networks |
maxResultSizeChars | int | The Python SDK passes this to the CLI via _meta["anthropic/maxResultSizeChars"] to relax the tool result length limit |
tools, allowed_tools, disallowed_tools, permission_mode, can_use_tool, and hooks. The annotations field names echoed in get_mcp_status() / MCP status may also be the CLI-projected readOnly, destructive, openWorld, rather than the original MCP *Hint names.
Step 2: Register with an MCP Server
create_sdk_mcp_server() registers one or more tools as an in-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, defaults to "1.0.0" |
tools | [search_docs, lookup_order] | Tools registered to this server |
McpSdkServerConfig, which can be passed directly to QoderAgentOptions.mcp_servers.
For the complete return type, see Tools Reference - create_sdk_mcp_server().
create_sdk_mcp_server() performs synchronous validation:
- The server name must be a non-empty string.
- Tool names must be non-empty strings.
- Tool descriptions must be non-empty strings.
- The same server cannot have duplicate tool names.
Step 3: Attach to query()
After you put the server in options.mcp_servers, the CLI discovers its tools and calls back into your handler through the SDK when the model needs them.
orders and the tool name is lookup_order, the full name is mcp__orders__lookup_order. Use this full name in allowed_tools, disallowed_tools, can_use_tool, hook matchers, and subagent tools configuration.
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 makes a dynamic decision before each tool call.
Permission Control Overview
| Method | Purpose | Granularity | Use case |
|---|---|---|---|
tools | Limits the visible tool set for this session | Session | Narrow the tools the model can see at the source |
allowed_tools / disallowed_tools | Pre-authorizes or denies specific tools | Tool | You know exactly which tools to allow or deny |
permission_mode | Sets the default permission policy for the whole session | Global | Quickly switch plan mode, accept edits, or skip permissions |
can_use_tool | Runs custom logic before each call | Call | Decide dynamically based on argument content |
hooks["PreToolUse"] | Intercepts tool calls through the hooks lifecycle | Call | You already use the hooks system and want shared auditing or blocking |
tools to narrow the visible set, allowed_tools / disallowed_tools for static rules, and can_use_tool for argument-level decisions.
Method 1: tools, allowed_tools, disallowed_tools
tools controls the tools visible to this session. allowed_tools and disallowed_tools control permission rules. Custom MCP tools must use full names.
Method 2: permission_mode
permission_mode sets the default permission behavior for the whole session with a single line.
| Mode | Effect |
|---|---|
"default" | Standard permission behavior; sensitive operations are handled by rules or runtime policy |
"acceptEdits" | Automatically accepts file edit operations; other sensitive operations still follow the permission policy |
"bypassPermissions" | Skips permission checks; must also set allow_dangerously_skip_permissions=True |
"yolo" | Compatibility alias for "bypassPermissions"; must also set allow_dangerously_skip_permissions=True |
"plan" | Plan mode, suitable for asking the model to produce a plan first |
"dontAsk" | Does not ask interactively; operations that are not pre-authorized or allowed by rules are denied |
"auto" | Runtime capability decides allow or deny automatically |
Method 3: can_use_tool
can_use_tool runs before a tool call. You can allow or deny based on the tool name, arguments, and approval context.
| Return | Effect |
|---|---|
PermissionResultAllow() | Allows execution with the original arguments |
PermissionResultAllow(updated_input={...}) | Allows execution and replaces tool arguments |
PermissionResultDeny(message="reason") | Denies execution; the model can see the reason and try another way |
PermissionResultDeny(message="reason", interrupt=True) | Denies and interrupts the current agent loop |
ToolPermissionContext include tool_use_id, agent_id, signal, title, display_name, description, suggestions, blocked_path, and decision_reason. For a more complete permission strategy, see Permissions.
When using custom tools in a subagent, also use the full tool name:
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".
How the SDK Handles Tool Errors
Tool handlers have three error paths.Business Failure: Return is_error: True
For expected business failures, return is_error: True. The SDK converts the result into an MCP CallToolResult and passes it to the CLI. The model can see the failure content and may retry or choose another path.
is_error: True:
- 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 raises an exception, the MCP layer converts it into an error result; the agent loop does not crash because of an ordinary tool exception. However, the model usually only sees the exception message — its format and content are less controllable than explicitly returningis_error: True.
is_error: True for predictable business failures, and raise only for truly unexpected exceptions.
Malformed Returns: SDK Wraps as Errors
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
A tool handler returns a dict, which the SDK converts into MCP’sCallToolResult. Text content is the most common:
| Type | Shape | Python SDK behavior |
|---|---|---|
| Text | {"type": "text", "text": "..."} | Converted to TextContent |
| Image | {"type": "image", "data": "...", "mimeType": "image/png"} | Converted to ImageContent; data is base64 |
| Resource link | {"type": "resource_link", "uri": "...", "name": "...", "description": "..."} | Degrades to text, concatenating name / uri / description for the model |
| Embedded text resource | {"type": "resource", "resource": {"text": "..."}} | Converted 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. - All fields in a Python simple-dict schema are required; use
TypedDict + NotRequiredor a full JSON Schema for optional fields. - Use a full JSON Schema dict when you need enums, numeric ranges, nested objects, or string pattern/format.
- Handlers must be
async defand return a dict with a"content"list. - Tool descriptions should explain “when to use it, what it does, what it returns” — avoid vague descriptions like
queryorhelper. readOnlyHintis tool metadata and a scheduling/permission 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
- MCP Integration: in-process, stdio, SSE, HTTP, OAuth, and other MCP server integration methods.
- Permissions:
permission_mode,allowed_tools,can_use_tool, hooks, and permission rule updates. - Hooks:
PreToolUse,PostToolUse,PermissionRequest, and other lifecycle extensions. - Subagent Guide: Let different agents use different tool sets.