Documentation Index
Fetch the complete documentation index at: https://docs.qoder.com/llms.txt
Use this file to discover all available pages before exploring further.
Tools are capabilities the model can call while executing a task. The Qoder Agent SDK supports two kinds of tools:
- 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() and createSdkMcpServer(), then exposed to the model.
This guide focuses on defining custom tools. For the complete built-in tool list, see Tools Reference - Built-in Tool List.
When using built-in tools, you do not implement the tools yourself. You only control which tools are available, which tools are pre-authorized, and which tools are denied for the current session through query() options.
query({
prompt: 'Read this repository and summarize risks in the authentication module. Do not modify files.',
options: {
auth: accessTokenFromEnv(),
cwd: '/path/to/project',
tools: ['Read', 'Grep', 'Glob'],
allowedTools: ['Read', 'Grep', 'Glob'],
},
});
Common built-in tools include Read, Edit, Write, Bash, Glob, Grep, WebFetch, WebSearch, and Agent. The complete list and names are defined by Tools Reference - Built-in Tool List. Input and output structures are documented in Built-in Tool Input and Output Types, such as FileReadInput / FileReadOutput, BashInput / BashOutput, and AgentInput / AgentOutput. If you need TypeScript-level unified representations, see ToolInputSchemas and ToolOutputSchemas.
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 a tool with
tool().
- Register the tool in an MCP server with
createSdkMcpServer().
- Attach the server through
mcpServers in query({ options }), and control calls with permission settings.
First, here is a complete minimal example. The following sections then explain each step.
import {
accessTokenFromEnv,
createSdkMcpServer,
query,
tool,
} from '@qoder-ai/qoder-agent-sdk';
import { z } from 'zod';
const lookupOrder = tool(
'lookup_order',
'Look up an order by order ID.',
{
orderId: z.string().describe('Order ID, such as O-1001'),
},
async ({ orderId }) => {
const order = await orders.find(orderId);
if (!order) {
return {
isError: true,
content: [{ type: 'text', text: `Order not found: ${orderId}` }],
};
}
return {
content: [{ type: 'text', text: JSON.stringify(order) }],
};
},
{ annotations: { readOnlyHint: true } },
);
const orderTools = createSdkMcpServer({
name: 'orders',
tools: [lookupOrder],
});
const messages = query({
prompt: 'Check the status of order O-1001 and summarize it in one sentence.',
options: {
auth: accessTokenFromEnv(),
mcpServers: { orders: orderTools },
allowedTools: ['mcp__orders__lookup_order'],
},
});
for await (const message of messages) {
if (message.type === 'result') {
console.log(message.result);
}
}
This step defines the tool itself: its name, description, input parameters, execution logic, and metadata.
tool() defines a tool. It has five arguments. See tool() for the complete type.
| Argument | Type | Required | Meaning |
|---|
name | string | Yes | Unique tool identifier within the current MCP server |
description | string | Yes | Tool description for the model; explain when to use it, what it does, and what it returns |
inputSchema | Schema extends AnyZodRawShape | Yes | Zod raw shape that defines tool input parameters |
handler | Function | Yes | Async execution function that receives parsed parameters and returns CallToolResult |
extras | ToolExtras | No | Extra tool metadata, currently used for annotations |
inputSchema takes a Zod raw shape, meaning a field object, not z.object(...). See AnyZodRawShape for the type and InferShape for handler parameter inference.
{
query: z.string().describe('Search keywords'),
maxResults: z.number().int().min(1).max(10).optional()
.describe('Maximum number of snippets to return'),
source: z.enum(['docs', 'tickets', 'wiki']).default('docs')
.describe('Where to search'),
}
Common patterns:
| Need | Pattern |
|---|
| 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) |
extras.annotations passes MCP tool annotations. The SDK keeps them on the tool definition and forwards them when registering the tool with the MCP server. See ToolExtras and ToolAnnotations for the complete fields.
| Field | Type | Optional | Meaning |
|---|
title | string | Yes | Human-readable title for the tool |
readOnlyHint | boolean | Yes | Marks the tool as read-only and not modifying state |
destructiveHint | boolean | Yes | Marks that the tool may modify or delete data |
openWorldHint | boolean | Yes | Marks that the tool accesses external systems or networks |
These fields do not replace permission configuration. Whether a tool call is allowed is still determined by tools, allowedTools, disallowedTools, permissionMode, canUseTool, and hooks. Current mcpServerStatus().tools[] does not echo annotations back to the host application. If your host UI needs to show this information, keep your own mapping next to the tool definition.
Example:
tool(
'search_docs',
'Search internal product documentation.',
{ query: z.string().describe('Search keywords') },
async ({ query }) => ({ content: [{ type: 'text', text: query }] }),
{
annotations: {
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
},
);
Step 2: Register with an MCP Server
createSdkMcpServer() 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.
const kbTools = createSdkMcpServer({
name: 'kb',
version: '1.0.0',
tools: [searchDocs],
});
| 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 |
See CreateSdkMcpServerOptions for the full option type and createSdkMcpServer() return value for the return value.
Step 3: Attach to query()
After you put the server in options.mcpServers, the CLI discovers its tools and calls back into your handler through the SDK when the model needs them.
query({
prompt: 'Search docs for the refund policy and summarize it.',
options: {
auth: accessTokenFromEnv(),
mcpServers: { kb: kbTools },
allowedTools: ['mcp__kb__search_docs'],
},
});
The full custom tool name format is:
mcp__{serverName}__{toolName}
For example, if the server name is orders and the tool name is lookup_order, the full name is mcp__orders__lookup_order. Use this full name in allowedTools, disallowedTools, canUseTool, hook matchers, and subagent tools configuration.
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 | Purpose | Granularity | Use case |
|---|
tools | Limits the visible tool set for this session | Session | Narrow the tools the model can see at the source |
allowedTools / disallowedTools | Pre-authorizes or denies specific tools | Tool | You know exactly which tools to allow or deny |
permissionMode | Sets the default permission policy for the whole session | Global | Quickly switch plan mode, accept edits, or skip permissions |
canUseTool | Runs custom logic before each call | Call | Decide dynamically based on arguments |
hooks.PreToolUse | Intercepts tool calls through the hooks lifecycle | Call | You already use hooks and want shared auditing or blocking |
These methods can be combined. A common pattern is to use tools to narrow the visible set, allowedTools / disallowedTools for static rules, and canUseTool for argument-level decisions.
tools controls the tools visible to this session. allowedTools and disallowedTools control permission rules. Custom MCP tools must use full names.
// Only expose read/search tools to this session.
query({
prompt: 'Analyze the repository without editing files.',
options: {
tools: ['Read', 'Glob', 'Grep'],
allowedTools: ['Read', 'Glob', 'Grep'],
},
});
// Explicitly deny high-risk tools.
query({
prompt: 'Review the project and report issues.',
options: {
disallowedTools: ['Bash', 'Write', 'Edit'],
},
});
// Use full names for custom MCP tools.
query({
prompt: 'Check order O-1001.',
options: {
mcpServers: { orders: orderTools },
allowedTools: ['mcp__orders__lookup_order'],
},
});
// Disable all tools. The model can only answer from its context.
query({
prompt: 'Explain what this SDK does at a high level.',
options: { tools: [] },
});
When the same tool matches both allow and deny rules, the deny rule takes precedence.
Method 2: permissionMode
permissionMode sets the default permission behavior for the whole session with one option.
query({
prompt: 'Refactor the code.',
options: {
permissionMode: 'acceptEdits',
},
});
| 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 allowDangerouslySkipPermissions: true |
'yolo' | Compatibility alias for 'bypassPermissions'; must also set allowDangerouslySkipPermissions: 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 |
canUseTool runs before a tool call. You can allow or deny based on the tool name, arguments, and approval context.
query({
prompt: 'Check order O-1001.',
options: {
auth: accessTokenFromEnv(),
mcpServers: { orders: orderTools },
allowedTools: ['mcp__orders__lookup_order'],
async canUseTool(toolName, input, options) {
if (toolName !== 'mcp__orders__lookup_order') {
return {
behavior: 'deny',
message: 'Only order lookup is allowed in this workflow.',
toolUseID: options.toolUseID,
};
}
return {
behavior: 'allow',
updatedInput: input,
toolUseID: options.toolUseID,
};
},
},
});
Common return values:
| Return | Effect |
|---|
{ behavior: 'allow' } | Allows execution with the original arguments |
{ behavior: 'allow', updatedInput: {...} } | Allows execution and replaces tool arguments |
{ behavior: 'deny', message: 'reason' } | Denies execution; the model can see the reason and try another way |
{ behavior: 'deny', message: 'reason', interrupt: true } | Denies and interrupts the current agent loop |
When using custom tools in a subagent, use the full tool name as well:
query({
prompt: 'Use the order-support agent to check order O-1001.',
options: {
auth: accessTokenFromEnv(),
mcpServers: { orders: orderTools },
allowedTools: ['Agent'],
agents: {
'order-support': {
description: 'Handles order lookup and explains order status.',
prompt: 'Use order tools to answer order status questions clearly.',
tools: ['mcp__orders__lookup_order'],
},
},
},
});
If you already use the hooks system, use PreToolUse to intercept or audit tool calls in one place.
query({
prompt: 'Run the test command.',
options: {
allowedTools: ['Bash'],
hooks: {
PreToolUse: [
{
matcher: 'Bash',
hooks: [
async (input) => {
const command = (input.tool_input as { command: string }).command;
if (command.includes('rm -rf')) {
return {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: 'rm -rf is not allowed',
},
};
}
return {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'allow',
},
};
},
],
},
],
},
},
});
For canUseTool parameter structure, see CanUseToolOptions. For return structure, see PermissionResult. For a more complete permission strategy, see Permissions.
Tool handlers have two error paths.
Business Failure: Return isError: true
For expected business failures, return isError: true. The SDK passes this CallToolResult to the CLI. The model can see the failure content and may retry or choose another path.
return {
isError: true,
content: [{
type: 'text',
text: JSON.stringify({
error: 'VALIDATION_ERROR',
message: 'Only SELECT statements are allowed.',
}),
}],
};
Good cases for isError: true:
- Arguments are valid, but no business result exists, such as an order not found.
- A security policy rejects execution, such as only allowing
SELECT queries.
- An external service returns a business error that can be explained.
Unexpected Exception: Handler Throws
If a handler throws, the MCP layer converts the exception into an error result, and the agent loop does not crash just because a normal tool exception occurred. However, the model usually only sees the exception message, which is less controllable than explicitly returning isError: true.
const toolThatMayThrow = tool(
'fetch_user',
'Fetch a user by ID.',
{ userId: z.string() },
async ({ userId }) => {
const response = await userService.fetch(userId);
if (!response.ok) {
throw new Error('User service failed');
}
return { content: [{ type: 'text', text: await response.text() }] };
},
);
Recommendation: use isError: true for predictable business failures, and throw only for truly unexpected exceptions.
Tool handlers return MCP CallToolResult. Text content is the most common:
return {
content: [{ type: 'text', text: 'done' }],
};
You can also return structured JSON strings, which help the model understand and continue processing:
return {
content: [{
type: 'text',
text: JSON.stringify({
orderId: 'O-1001',
status: 'shipped',
eta: '2026-05-20',
}),
}],
};
Common content blocks; see McpToolResultContent for the complete union type:
| 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 |
| Resource link | { type: 'resource_link', uri, name?, description?, mimeType? } | Returns a referenceable resource |
| Embedded resource | { type: 'resource', resource } | Returns text or binary resource content |
See Tools Reference - CallToolResult for complete type definitions.
Common Pitfalls
- When writing permission configuration for custom tools, use the full
mcp__server__tool name.
- Pass a Zod raw shape as the third argument to
tool(), not z.object(...).
- Tool descriptions should explain when to use the tool, what it does, and what it returns. Avoid vague descriptions like
query or helper.
readOnlyHint is 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(), createSdkMcpServer(), CallToolResult, and built-in tool input/output types.
- MCP Integration: stdio, SSE, HTTP, OAuth, and other MCP server integration methods.
- Permissions:
permissionMode, allowedTools, canUseTool, and permission rule updates.
- Subagent Guide: Let different agents use different tool sets.