* 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>
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
pathToQwenExecutableoption to the full path of theqwenbinary.
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 likeRead,Edit, andBashalso work, but invocation specifiers such asBash(git *)are stripped.coreToolsrestricts 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 viacanUseToolcallback or inallowedTools. 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.
excludeTools/permissions.deny- Blocks tools completely (returns permission error)permissions.ask- Always requires user confirmationpermissionMode: 'plan'- Blocks all non-read-only toolspermissionMode: 'yolo'- Auto-approves all toolsallowedTools/permissions.allow- Auto-approves matching toolspermissionMode: 'auto'- Classifier-mediated approval for remaining toolscanUseToolcallback - Custom approval logic (if provided, not called for allowed tools)- 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
}
}