Skip to main content

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.

Built-in Tools

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.

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:
  1. Create a tool with tool().
  2. Register the tool in an MCP server with createSdkMcpServer().
  3. Attach the server through mcpServers in query({ options }), and control calls with permission settings.

Custom Tool Integration Steps

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);
  }
}

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() defines a tool. It has five arguments. See tool() for the complete type.
ArgumentTypeRequiredMeaning
namestringYesUnique tool identifier within the current MCP server
descriptionstringYesTool description for the model; explain when to use it, what it does, and what it returns
inputSchemaSchema extends AnyZodRawShapeYesZod raw shape that defines tool input parameters
handlerFunctionYesAsync execution function that receives parsed parameters and returns CallToolResult
extrasToolExtrasNoExtra tool metadata, currently used for annotations

Configure Input Parameters

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:
NeedPattern
Required stringz.string().describe('...')
Optional parameterz.string().optional().describe('...')
Default valuez.number().default(5)
Enumz.enum(['docs', 'tickets'])
Numeric rangez.number().min(1).max(10)

Configure Tool Metadata

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.
FieldTypeOptionalMeaning
titlestringYesHuman-readable title for the tool
readOnlyHintbooleanYesMarks the tool as read-only and not modifying state
destructiveHintbooleanYesMarks that the tool may modify or delete data
openWorldHintbooleanYesMarks 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],
});
FieldHow to set itDescription
nameFor example kb, ordersServer name; forms full tool names like mcp__{name}__{tool}
versionFor 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.

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

MethodPurposeGranularityUse case
toolsLimits the visible tool set for this sessionSessionNarrow the tools the model can see at the source
allowedTools / disallowedToolsPre-authorizes or denies specific toolsToolYou know exactly which tools to allow or deny
permissionModeSets the default permission policy for the whole sessionGlobalQuickly switch plan mode, accept edits, or skip permissions
canUseToolRuns custom logic before each callCallDecide dynamically based on arguments
hooks.PreToolUseIntercepts tool calls through the hooks lifecycleCallYou 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.

Method 1: tools, allowedTools, disallowedTools

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',
  },
});
ModeEffect
'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

Method 3: canUseTool

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:
ReturnEffect
{ 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'],
      },
    },
  },
});

Method 4: hooks.PreToolUse

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.

How the SDK Handles Tool Errors

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 Return Values

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:
TypeShapeDescription
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.