qwen-code/docs/developers/sdk-typescript.md
易良 f2e593c26b
fix(core): make permissions.allow restrict the tool schemas sent to the model (#9829)
* fix(core): make permissions.allow restrict the tool schemas sent to the model (#9827)

permissions.allow only auto-approved calls; it never gated tool
registration, so the outgoing tools array kept every built-in schema
even when an allowlist was configured — contradicting the settings
docs migration table ("unlisted tools are disabled at registry
level") and breaking backends like llama.cpp that compile all tool
schemas into a single grammar.

- Activate a registry-level allowlist when settings.permissions.allow
  has at least one valid rule: built-in tools not covered by any allow
  rule are no longer registered (absent from /tools and the API
  request). MCP tools and the structured_output contract stay exempt;
  session-granted rules ("always allow", skill allowedTools) extend
  membership but never activate the allowlist mid-session.
  --allowed-tools / SDK allowedTools / legacy tools.allowed keep their
  pure auto-approval semantics.
- Complete the rule alias map so the display names shown by /tools
  (SendMessage, UpdateGoal, ...) match in allow/deny rules.

* fix(core): honor permissions.allow in the list_directory opt-in gate (#9827)

isLsToolEnabled() only read tools.listDirectory.enabled and the coreTools
allowlist, so an explicitly allowlisted list_directory passed
PermissionManager.isToolEnabled() but was never registered — absent from
/tools and the model request, with calls failing TOOL_NOT_REGISTERED. This
broke the documented tools.core -> permissions.allow migration equivalence
for exactly this tool. Consult getRegistryAllowList() with the same
coverage semantics the registry gate uses (toolMatchesRuleToolName, so
Read / ListFiles / specifier forms all count).

* fix(core): keep plan-mode lifecycle tools registered under the allowlist (#9827)

The permissions.allow registry gate covered exit_plan_mode /
enter_plan_mode / ask_user_question, so the exact reporter configuration
unregistered them. The plan-mode system reminder still instructs the model
to present its plan by calling exit_plan_mode, whose schema is then never
sent, so the sanctioned plan flow cannot complete. Exempt the three
plan-mode lifecycle tools alongside structured_output (same synthetic-
system-tool class the CORE_TOOLS docstring names; deny rules still apply).

* docs(sdk): correct allowedTools registry-allowlist contract (#9827)

The JSDoc added for QueryOptions.allowedTools (and the coreTools block)
claimed the SDK allowedTools param activates the registry allowlist and
hides unlisted built-in schemas. It does not: ProcessTransport maps it to
the CLI --allowed-tools flag, and this PR's CLI wiring builds
registryAllowList only from settings.permissions.allow. Reword both JSDoc
blocks and the two hand-maintained SDK doc pages (sdk-typescript.md,
sdk-typescript/README.md) to the shipped contract: allowedTools stays a
pure auto-approval grant; only permissions.allow in settings.json
(requires restart) activates the registry allowlist.

* docs(settings): note plan-mode lifecycle exemption in the allowlist (#9827)

The permissions.allow registry-allowlist exemption list named only MCP
tools and the structured_output contract. Add the plan-mode lifecycle
tools (exit_plan_mode / enter_plan_mode / ask_user_question) exempted in
b8ba258c40 so the documented exemption set matches the gate.

* fix(core): exempt the computer_use__* family from the registry allowlist (#9827)

* fix(core): gate command-discovered tools through the registry allowlist (#9827)

* fix(core): make registry-allowlist membership monotonic within the session (#9827)

* fix(core): narrow the skill allowedTools grant contract to restart-scoped registration (#9827)

* fix(core): count ask rules toward registry-allowlist membership (#9827)

A tool covered only by a permissions.ask rule was silently deregistered
whenever the permissions.allow registry allowlist was active: allow
["ReadFile"] + ask ["Shell"] hid the whole shell family from the model,
so the documented "always require user confirmation" silently became
"tool unavailable" and the ask rule could never fire.

Ask rules express "this tool must stay usable, with confirmation", so
they now count toward registry membership (frozen at startup for the
same restart-scoped monotonicity as allow rules).

* docs(settings): note that ask rules keep tools registered under allowlist (#9827)

* test(cli): pin registry-allowlist strip in bare mode (#9827)

The wiring tests only covered the safe-mode half of
registryAllowList: bareMode || safeMode ? undefined : ... — a mutant
dropping the bareMode guard survived the suite and would activate the
allowlist from settings while bare mode strips those same rules from
the merged allow set, leaving the bare registry's minimal toolset
ungated. Mirror the safe-mode test for --bare.

* fix(core): attribute registry-allowlist misses to permissions.allow (#9827)

An allowlist-miss rejection surfaced as "Qwen Code requires permission
to use X, but that permission was declined" citing a deny rule that
does not exist (findMatchingDenyRule finds nothing) and never
mentioning permissions.allow. When no deny rule matched and the
registry allowlist is active, emit a distinct message pointing at the
real config knob.

* test(core): pin resolveToolName coverage of every ToolNames entry (#9827)

TOOL_NAME_ALIASES hand-maintains the canonical/display-name mappings
that tool-names.ts declares; nothing enforced the sync, so a tool added
to tool-names.ts without an alias entry would compile, pass every test,
and silently never match a permission rule — the exact #9827 bug class,
now with higher stakes since a missed entry also breaks allowlist
coverage. Walk every ToolNames/ToolDisplayNames pair and assert it
round-trips through resolveToolName.

* fix(core): expose isPermissionsAllowListActive on scoped PM shims (#9827)

* fix(core): honour ask-only list_directory coverage in the opt-in gate (#9827)

* docs: align registry-allowlist contract wording across docs and JSDoc (#9827)

* docs: scope settings.md removal and whole-tool-deny claims precisely (#9827)

* fix(core): count merged allow coverage in the list_directory opt-in gate (#9827)

isLsToolEnabled() scanned only the settings-sourced getRegistryAllowList() for allow coverage while PermissionManager.isToolEnabled() counts the merged allow set (settings + --allowed-tools + SDK allowedTools + legacy tools.allowed). Under an active allowlist, list_directory covered only by a merged rule passed isToolEnabled but was never offered to registerLazy — it vanished from /tools and the model request while calls failed TOOL_NOT_REGISTERED. Count the merged allow set for coverage (activation still requires a valid settings rule) and filter empty/whitespace-only entries from activation exactly like PermissionManager.initialize's parseRules does, so a degenerate [""] entry cannot activate the gate here while the permission system reports the allowlist inactive.

* test(core): pin activation source and merged-allow coverage of the list_directory gate (#9827)

Every existing isLsToolEnabled test fed the identical array as both allow and registryAllowList, so the settings-only vs merged-allow distinction was unpinned and the R4-1 divergence shipped uncovered. Add three cases shaped like the CLI wiring: coverage by a merged (non-settings) allow rule under an active allowlist registers the tool; merged-only coverage with no settings rule does not activate the allowlist; an empty settings entry ([""]) does not activate it either.

* fix(core): attribute scheduler denials to permissions.allow only for uncovered tools (#9827)

The allowlist-miss message fired for any disabled tool with no matching deny rule while the allowlist is active — including tools rejected by the legacy coreTools gate that ARE covered by an allow rule, where 'not covered by any permissions.allow rule' is wrong and the remediation a no-op. Expose isCoveredByAllowOrAskRule on PermissionManager and take the allowlist branch only when the tool is genuinely uncovered; covered tools fall back to the generic declined message. The optional call keeps scoped PermissionManager shims (installed via 'as unknown as PermissionManager') from throwing until they grow the delegation.

* test(core): pin the covered-tool fallback for scheduler denial messages (#9827)

Add a scheduler-level case where the allowlist is active, no deny rule matches, and the disabled tool IS covered by an allow rule (the legacy coreTools gate shape): the message must be the generic declined one, not the permissions.allow attribution. Also make the existing allowlist-miss stub explicit about coverage.

* fix(core): register request_shutdown in the permission rule alias map (#9827)

Merging origin/main brought ToolNames.REQUEST_SHUTDOWN (#9806) but no TOOL_NAME_ALIASES entry, which the resolveToolName exhaustiveness test added on this branch pins. Map request_shutdown / RequestShutdown so permission rules can address the tool.

* fix(core): guard the list_directory allowlist gate against non-string rules (#9827)

isLsToolEnabled()'s activation check and coverage scan called raw.trim() / parseRule(raw) directly while PermissionManager.initialize computes the same thing through parseRules, whose r && r.trim() filter skips falsy entries. Settings load performs no element-type validation (the schema declares only type: array), so a stray null in settings.permissions.allow/ask — or in the legacy tools.allowed key riding the merged coverage set — threw TypeError during createToolRegistry and crashed startup while PermissionManager.initialize tolerated the same settings file. Mirror the parseRules guard with a typeof check in both the activation check and the coverage predicate, and pin both arms (tolerated entries still activate/cover, a [null]-only list keeps the gate closed).

* fix(core): exempt task_stop from the permissions.allow registry gate (#9827)

task_stop satisfies the PR's own two written exemption criteria but was missing from the set: it is shouldDefer=true (task-stop.ts), the exact deferred-schema property the computer_use__* exemption cites, and it is advertised to the model by a registered tool's copy — run_shell_command's schema says to use task_stop to stop a background command (and not to use broad process-name kills), and the background-promotion result instructs task_stop({ task_id }) verbatim. Under the reporter configuration the suite pins, run_shell_command stays listed while task_stop was gated out, so the sanctioned stop flow failed. Add the exemption and pin it next to the plan-mode exemption tests, including that a whole-tool deny rule still wins via the existing evaluate pass.

* fix(core): keep shim denials on the pre-#9827 message when coverage is unknown (#9827)

The optional isCoveredByAllowOrAskRule call's : true fallback routed shim-mediated rejections of COVERED tools into the new allowlist-attribution message, contradicting the comment above it ('they keep the pre-#9827 message meanwhile'). Both production shims (memory-scoped-agent-config.ts, skillReviewAgentPlanner.ts) Pick a partial interface without isCoveredByAllowOrAskRule, so for them the ternary always took the allowlist arm — telling the user a covered tool 'is not covered by any permissions.allow rule' when a different gate (e.g. the legacy coreTools allowlist) rejected it. Flip the fallback to false so unknown coverage stays on the pre-#9827 declined message, and update the shim test to pin that message instead of the allowlist one.

* test(core): pin that ask-only rules never activate the allowlist (#9827)

The suite pins ask rules counting toward allowlist membership, but nothing pins the complementary activation boundary: no test constructed a PermissionManager with only permissionsAsk (no permissionsAllow) and asserted the allowlist stays inactive. Current behavior is correct; this guards against a future edit folding ask rules into activation, which would turn an ask-only posture (permissions.ask: ["Shell"], no allow rules — a natural 'always confirm shell' config) into an active allowlist that deregisters every unlisted built-in. The nearest existing test ('no allow rules → allowlist inactive') uses no rules at all and would still pass.

* fix(core): exempt tool_search from the permissions.allow registry gate (#9827)

Under a narrow active allowlist, tool_search itself was gated out of the
registry. Without ToolSearch, client.ts resolveDeferredToolsForReminder
eagerly force-reveals every registered deferred tool (all mcp__* and the
deferred computer_use__* family) into the eager model request, and
preloadDeferredToolsWithinBudget early-returns — inverting the
schema-shrink goal into maximal schema bloat for exactly the deferred
families the other exemptions preserve for ToolSearch discoverability.
Pre-#9827 tool_search always bypassed the legacy coreTools gate as a
non-core tool.

* test(core): pin the deny-rule arm's precedence in the scheduler permission message (#9827)

The three-way message branch in CoreToolScheduler covers the allowlist-miss
arm and the generic fallback arm, but every findMatchingDenyRule mock
returned undefined, so the deny-rule arm — whose position FIRST in the
if/else-if chain is what makes a real denial cite the matching rule instead
of the allowlist attribution — had no scheduler-level coverage. Add two
tests where findMatchingDenyRule returns a matching rule: one with the
allowlist arm armed (active allowlist + uncovered tool) pinning the
if/else-if ordering, one without an active allowlist pinning the deny arm
over the generic declined fallback. Mutation-checked: disabling the deny
arm fails both tests.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(core): pin deny/ask sibling semantics at the discovery gate (#9827)

The discovery-gate test built its PermissionManager with EMPTY ask/deny
lists, so the gate's two documented sibling semantics were unpinned:
settings.md says a whole-tool deny rule removes a discovered tool from
the registry even under an active allowlist, and an ask rule keeps a
discovered tool registered ("always require confirmation" must never
silently become "tool unavailable"). Add two discovery-gate tests with
deny-covered and ask-covered PermissionManager configurations: the denied
tool is also allow-covered so only the deny branch of isToolEnabled can
reject it, and the ask test carries an uncovered control tool proving the
gate is active in the same run. Mutation-checked: ignoring deny decisions
fails the deny test only; dropping ask coverage from
isCoveredByAllowOrAskRule fails the ask test only.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs: match the allowlist activation wording to the real predicate (#9827)

Four surfaces said the permissions.allow registry allowlist activates
"when at least one allow rule is configured", but
PermissionManager.initialize computes activation as at least one VALID
rule from settings.permissions.allow only (getRegistryAllowList): a
malformed entry never activates it, and auto-approval-only sources such
as the --allowed-tools CLI flag / the SDK allowedTools parameter never
do either. Reword settings.md, the SDK docs, the sdk-typescript README
and the coreTools JSDoc to the exact predicate, and complete their
exemption lists with task_stop and tool_search, which isToolEnabled
exempts but the docs did not name. Docs-only.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-25 11:57:24 +00:00

34 KiB

Typescript SDK

@qwen-code/sdk

A minimum experimental TypeScript SDK for programmatic access to Qwen Code.

Feel free to submit a feature request/issue/PR.

Installation

npm install @qwen-code/sdk

Requirements

  • Node.js >= 22.0.0
  • Qwen Code >= 0.4.0 (stable) installed and accessible in PATH

Note for nvm users: If you use nvm to manage Node.js versions, the SDK may not be able to auto-detect the Qwen Code executable. You should explicitly set the pathToQwenExecutable option to the full path of the qwen binary.

Quick Start

import { query } from '@qwen-code/sdk';

// Single-turn query
const result = query({
  prompt: 'What files are in the current directory?',
  options: {
    cwd: '/path/to/project',
  },
});

// Iterate over messages
for await (const message of result) {
  if (message.type === 'assistant') {
    console.log('Assistant:', message.message.content);
  } else if (message.type === 'result') {
    console.log('Result:', message.result);
  }
}

API Reference

query(config)

Creates a new query session with the Qwen Code.

Parameters

  • prompt: string | AsyncIterable<SDKUserMessage> - The prompt to send. Use a string for single-turn queries or an async iterable for multi-turn conversations.
  • options: QueryOptions - Configuration options for the query session.

QueryOptions

Option Type Default Description
cwd string process.cwd() The working directory for the query session. Determines the context in which file operations and commands are executed.
model string - The AI model to use (e.g., 'qwen-max', 'qwen-plus', 'qwen-turbo'). Takes precedence over OPENAI_MODEL and QWEN_MODEL environment variables.
pathToQwenExecutable string Auto-detected Path to the Qwen Code executable. Supports multiple formats: 'qwen' (native binary from PATH), '/path/to/qwen' (explicit path), '/path/to/cli.js' (Node.js bundle), 'node:/path/to/cli.js' (force Node.js runtime), 'bun:/path/to/cli.js' (force Bun runtime). If not provided, auto-detects from: QWEN_CODE_CLI_PATH env var, ~/.volta/bin/qwen, ~/.npm-global/bin/qwen, /usr/local/bin/qwen, ~/.local/bin/qwen, ~/node_modules/.bin/qwen, ~/.yarn/bin/qwen.
permissionMode 'default' | 'plan' | 'auto-edit' | 'auto' | 'yolo' 'default' Permission mode controlling tool execution approval. See Permission Modes for details.
canUseTool CanUseTool - Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 60 seconds or the request will be auto-denied. See Custom Permission Handler.
env Record<string, string> - Environment variables to pass to the Qwen Code process. Merged with the current process environment.
systemPrompt string | QuerySystemPromptPreset - System prompt configuration for the main session. Use a string to fully override the built-in Qwen Code system prompt, or a preset object to keep the built-in prompt and append extra instructions.
mcpServers Record<string, McpServerConfig> - MCP (Model Context Protocol) servers to connect. Supports external servers (stdio/SSE/HTTP) and SDK-embedded servers. External servers are configured with transport options like command, args, url, httpUrl, etc. SDK servers use { type: 'sdk', name: string, instance: Server }.
abortController AbortController - Controller to cancel the query session. Call abortController.abort() to terminate the session and cleanup resources.
debug boolean false Enable debug mode for verbose logging from the CLI process.
maxSessionTurns number -1 (unlimited) Maximum number of conversation turns before the session automatically terminates. Must be an integer. A turn consists of a user message and an assistant response.
coreTools string[] - Uses the legacy coreTools / CLI --core-tools allowlist semantics. If specified, only matching core tools are registered for the session. This is separate from permissions.allow in settings.json, which also activates a registry-level allowlist at startup: when at least one valid allow rule is configured there (malformed entries do not count), built-in tools not covered by any allow or ask rule are not registered (MCP tools, the --json-schema structured_output contract, the plan-mode lifecycle tools, task_stop, tool_search, and the computer_use__* family are exempt; requires restart, #9827). The SDK allowedTools parameter cannot activate the allowlist on its own, but while the allowlist is active its rules are merged into the effective allow set and count toward coverage, keeping covered built-ins registered. Example: ['read_file', 'edit', 'run_shell_command'].
excludeTools string[] - Equivalent to permissions.deny in settings.json. Excluded tools return a permission error immediately. Takes highest priority over all other permission settings. Supports tool name aliases and pattern matching: tool name ('write_file'), shell command prefix ('Bash(rm *)'), or path patterns ('Read(.env)', 'Edit(/src/**)').
allowedTools string[] - Equivalent to permissions.allow in settings.json for auto-approval. Matching tools bypass canUseTool callback and execute automatically. Only applies when tool requires confirmation. Unlike permissions.allow in settings.json, this parameter alone does not activate the registry allowlist; however, while a settings-provided allowlist is active, allowedTools rules are merged into the effective allow set and count toward coverage, so covered built-ins stay registered. Supports same pattern matching as excludeTools. Example: ['Bash(git status)', 'Bash(npm test)'].
authType 'openai' | 'qwen-oauth' 'openai' Authentication type for the AI service. Qwen OAuth free tier was discontinued on 2026-04-15; new SDK setups should use OpenAI-compatible authentication or another supported provider.
agents SubagentConfig[] - Configuration for subagents that can be invoked during the session. Subagents are specialized AI agents for specific tasks or domains.
includePartialMessages boolean false When true, the SDK emits incomplete messages as they are being generated, allowing real-time streaming of the AI's response.
resume string - Resume a previous session by providing its session ID. Equivalent to CLI's --resume flag.
sessionId string - Specify a session ID for the new session. Ensures SDK and CLI use the same ID without resuming history. Equivalent to CLI's --session-id flag.

Note

For coreTools, aliases like Read, Edit, and Bash also work, but invocation specifiers such as Bash(git *) are stripped. coreTools restricts tool registration, not invocation patterns.

Timeouts

The SDK enforces the following default timeouts:

Timeout Default Description
canUseTool 1 minute Maximum time for canUseTool callback to respond. If exceeded, the tool request is auto-denied.
mcpRequest 1 minute Maximum time for SDK MCP tool calls to complete.
controlRequest 1 minute Maximum time for control operations like initialize(), setModel(), setPermissionMode(), getContextUsage(), and interrupt() to complete.
streamClose 1 minute Maximum time to wait for initialization to complete before closing CLI stdin in multi-turn mode with SDK MCP servers.

You can customize these timeouts via the timeout option:

import { query } from '@qwen-code/sdk';

const q = query({
  prompt: 'Your prompt',
  options: {
    timeout: {
      canUseTool: 60000, // 60 seconds for permission callback
      mcpRequest: 600000, // 10 minutes for MCP tool calls
      controlRequest: 60000, // 60 seconds for control requests
      streamClose: 15000, // 15 seconds for stream close wait
    },
  },
});

Message Types

The SDK provides type guards to identify different message types:

import {
  isSDKUserMessage,
  isSDKAssistantMessage,
  isSDKSystemMessage,
  isSDKResultMessage,
  isSDKPartialAssistantMessage,
} from '@qwen-code/sdk';

for await (const message of result) {
  if (isSDKAssistantMessage(message)) {
    // Handle assistant message
  } else if (isSDKResultMessage(message)) {
    // Handle result message
  }
}

Query Instance Methods

The Query instance returned by query() provides several methods:

const q = query({ prompt: 'Hello', options: {} });

// Get session ID
const sessionId = q.getSessionId();

// Check if closed
const closed = q.isClosed();

// Interrupt the current operation
await q.interrupt();

// Change permission mode mid-session
await q.setPermissionMode('yolo');

// Change model mid-session
await q.setModel('qwen-max');

// Get context window usage breakdown (token counts per category)
const usage = await q.getContextUsage();
// Pass true to hint that per-item details should be displayed
const detail = await q.getContextUsage(true);

// Close the session
await q.close();

interrupt() cancels only the active turn. For a multi-turn query created with an async iterable prompt, the query and its input stream remain open, so later messages from the iterable are processed normally. Use close() or abort the configured AbortController when you want to end the entire session.

Daemon caller-supplied session IDs

DaemonClient.createOrAttachSession accepts an optional sessionId for callers that must persist an identity before session creation:

import { DaemonClient } from '@qwen-code/sdk';

const daemon = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170' });
const session = await daemon.createOrAttachSession({
  workspaceCwd: '/path/to/project',
  sessionId: '550E8400-E29B-41D4-A716-446655440000',
});

console.log(session.sessionId); // 550e8400-e29b-41d4-a716-446655440000

The SDK requires the daemon's session_id_override capability before sending the mutation. REST mode serializes sessionId directly; an active ACP adapter maps it to session/new._meta["qwen-code/sessionId"]. The SDK verifies the success response and throws DaemonSessionIdProtocolError if the daemon returns a different ID.

This option always creates a new thread session and is not an idempotent attach. If the create outcome is ambiguous, use the known ID with load or resume. Omitting the option preserves the existing create-or-attach behavior.

Permission Modes

The SDK supports different permission modes for controlling tool execution:

  • default: Write tools are denied unless approved via canUseTool callback or in allowedTools. Read-only tools execute without confirmation.
  • plan: Blocks all write tools, instructing AI to present a plan first.
  • auto-edit: Auto-approve edit tools (edit, write_file, notebook_edit) while other tools require confirmation.
  • auto: Uses the built-in classifier to auto-approve safe tool calls and block risky ones, with manual-approval fallback after repeated policy blocks or classifier outages.
  • yolo: All tools execute automatically without confirmation.

Permission Priority Chain

Decision priority (highest first): deny > ask > allow > (default/interactive mode)

The first matching rule wins.

  1. excludeTools / permissions.deny - Blocks tools completely (returns permission error)
  2. permissions.ask - Always requires user confirmation
  3. permissionMode: 'plan' - Blocks all non-read-only tools
  4. permissionMode: 'yolo' - Auto-approves all tools
  5. allowedTools / permissions.allow - Auto-approves matching tools
  6. permissionMode: 'auto' - Classifier-mediated approval for remaining tools
  7. canUseTool callback - Custom approval logic (if provided, not called for allowed tools)
  8. Default behavior - Auto-deny in SDK mode (write tools require explicit approval)

Examples

Multi-turn Conversation

import { query, type SDKUserMessage } from '@qwen-code/sdk';

async function* generateMessages(): AsyncIterable<SDKUserMessage> {
  yield {
    type: 'user',
    session_id: 'my-session',
    message: { role: 'user', content: 'Create a hello.txt file' },
    parent_tool_use_id: null,
  };

  // Wait for some condition or user input
  yield {
    type: 'user',
    session_id: 'my-session',
    message: { role: 'user', content: 'Now read the file back' },
    parent_tool_use_id: null,
  };
}

const result = query({
  prompt: generateMessages(),
  options: {
    permissionMode: 'auto-edit',
  },
});

for await (const message of result) {
  console.log(message);
}

Custom Permission Handler

import { query, type CanUseTool } from '@qwen-code/sdk';

const canUseTool: CanUseTool = async (toolName, input, { signal }) => {
  // Allow all read operations
  if (toolName.startsWith('read_')) {
    return { behavior: 'allow', updatedInput: input };
  }

  // Prompt user for write operations (in a real app)
  const userApproved = await promptUser(`Allow ${toolName}?`);

  if (userApproved) {
    return { behavior: 'allow', updatedInput: input };
  }

  return { behavior: 'deny', message: 'User denied the operation' };
};

const result = query({
  prompt: 'Create a new file',
  options: {
    canUseTool,
  },
});

With External MCP Servers

import { query } from '@qwen-code/sdk';

const result = query({
  prompt: 'Use the custom tool from my MCP server',
  options: {
    mcpServers: {
      'my-server': {
        command: 'node',
        args: ['path/to/mcp-server.js'],
        env: { PORT: '3000' },
      },
    },
  },
});

Override the System Prompt

import { query } from '@qwen-code/sdk';

const result = query({
  prompt: 'Say hello in one sentence.',
  options: {
    systemPrompt: 'You are a terse assistant. Answer in exactly one sentence.',
  },
});

Append to the Built-in System Prompt

import { query } from '@qwen-code/sdk';

const result = query({
  prompt: 'Review the current directory.',
  options: {
    systemPrompt: {
      type: 'preset',
      preset: 'qwen_code',
      append: 'Be terse and focus on concrete findings.',
    },
  },
});

With SDK-Embedded MCP Servers

The SDK provides tool and createSdkMcpServer to create MCP servers that run in the same process as your SDK application. This is useful when you want to expose custom tools to the AI without running a separate server process.

tool(name, description, inputSchema, handler)

Creates a tool definition with Zod schema type inference.

Parameter Type Description
name string Tool name (1-64 chars, starts with letter, alphanumeric and underscores)
description string Human-readable description of what the tool does
inputSchema ZodRawShape Zod schema object defining the tool's input parameters
handler (args, extra) => Promise<Result> Async function that executes the tool and returns MCP content blocks

The handler must return a CallToolResult object with the following structure:

{
  content: Array<
    | { type: 'text'; text: string }
    | { type: 'image'; data: string; mimeType: string }
    | { type: 'resource'; uri: string; mimeType?: string; text?: string }
  >;
  isError?: boolean;
}

createSdkMcpServer(options)

Creates an SDK-embedded MCP server instance.

Option Type Default Description
name string Required Unique name for the MCP server
version string '1.0.0' Server version
tools SdkMcpToolDefinition[] - Array of tools created with tool()

Returns a McpSdkServerConfigWithInstance object that can be passed directly to the mcpServers option.

Example

import { z } from 'zod';
import { query, tool, createSdkMcpServer } from '@qwen-code/sdk';

// Define a tool with Zod schema
const calculatorTool = tool(
  'calculate_sum',
  'Add two numbers',
  { a: z.number(), b: z.number() },
  async (args) => ({
    content: [{ type: 'text', text: String(args.a + args.b) }],
  }),
);

// Create the MCP server
const server = createSdkMcpServer({
  name: 'calculator',
  tools: [calculatorTool],
});

// Use the server in a query
const result = query({
  prompt: 'What is 42 + 17?',
  options: {
    permissionMode: 'yolo',
    mcpServers: {
      calculator: server,
    },
  },
});

for await (const message of result) {
  console.log(message);
}

Abort a Query

import { query, isAbortError } from '@qwen-code/sdk';

const abortController = new AbortController();

const result = query({
  prompt: 'Long running task...',
  options: {
    abortController,
  },
});

// Abort after 5 seconds
setTimeout(() => abortController.abort(), 5000);

try {
  for await (const message of result) {
    console.log(message);
  }
} catch (error) {
  if (isAbortError(error)) {
    console.log('Query was aborted');
  } else {
    throw error;
  }
}

Error Handling

The SDK provides an AbortError class for handling aborted queries:

import { AbortError, isAbortError } from '@qwen-code/sdk';

try {
  // ... query operations
} catch (error) {
  if (isAbortError(error)) {
    // Handle abort
  } else {
    // Handle other errors
  }
}