* feat(mcp): support MCP resources and reliably surface prompts
Prompts were silently hidden for MCP servers that implement `prompts/list`
but under-declare the `prompts` capability in their initialize response.
Drop the capability gate in `listMcpPrompts` (and apply the same leniency to
resources): always attempt the list call and swallow `Method not found`, so
those prompts now appear as `/` slash commands like in other clients.
Add first-class MCP resource support:
- core: `listMcpResources` / `discoverResources`, a `DiscoveredMCPResource`
type, a `ResourceRegistry`, and `Config.getResourceRegistry()`. Discovery
is wired into the standalone `McpClient.discover()` path and the
connection-pool path (snapshot -> `SessionMcpView.applyResources` -> the
session's registry, with a `resourcesChanged` event on reconnect). A
resource-only server now counts as a successful discovery.
- cli: the `/mcp` dialog shows per-server Resources (and Prompts) counts.
- cli: `@server:uri` reads an MCP resource and injects its contents into the
message (text inline, blobs as inlineData); `@server:` autocompletes the
server's resource URIs. The `server` prefix must match a configured MCP
server, so existing `@path` file references are unaffected.
Docs updated; unit tests added across core and cli.
* fix(mcp): reload commands on discovery + harden resource ref parsing
Address review feedback and complete the prompt UX:
- Slash commands now rebuild when an MCP server finishes connecting.
Discovery is progressive (runs after the UI is interactive), so prompts
a server exposes via prompts/list were registered too late to appear in
the `/` menu — the `/mcp` dialog showed the count while the slash menu
stayed empty. A debounced MCP-status listener now reloads the command
tree on connect. Verified end-to-end in a real TUI against a mock server
that under-declares the prompts capability: `/greet` now lists.
- `isMethodNotFound` keys off the JSON-RPC `-32601` code rather than the
server-supplied message text (which may be localized or worded
differently); applied to both `listMcpPrompts` and `listMcpResources`.
- `useAtCompletion` uses `Object.hasOwn` so `@__proto__:` / `@constructor:`
and other inherited keys are not mistaken for configured servers.
* fix(mcp): lenient resource read to match discovery + review polish
- `McpClient.readResource` no longer prechecks `getServerCapabilities()
?.resources`. This PR is the first caller to make the read path
reachable, and the strict precheck meant a server that answers
`resources/read` but under-declares the `resources` capability — exactly
the servers the lenient `listMcpResources` discovery targets — would get
its resources discovered, listed in `/mcp`, and autocompleted, yet fail
every `@server:uri` with a misleading "does not support resources". The
read now matches discovery; a server that truly lacks resources answers
`-32601`, surfaced as the existing error card. Added a regression test.
- `@server:uri` success cards now report what was injected ("Injected N
chars" / "N attachments") or "(no readable content)" when a read yields
no text/blob parts, so a partially-empty multi-ref read isn't hidden.
- `useAtCompletion` resource filter reduced to a single `includes` match
(`startsWith` was subsumed; empty partial matches all via `includes('')`);
the overstated "prefers prefix" comment is corrected.
- Tests: mixed `@file` + `@server:uri` injection (both parts + both cards),
empty-content card, and the pool restart fan-out now asserts
`applyResources` alongside `applyTools`.
* perf+security(mcp): parallelize discovery/reads, cap & frame resources
Review round 3 fold-ins:
- Discovery now runs `listMcpPrompts` / `listMcpResources` / `discoverTools`
concurrently in `discoverAndReturn` (independent reads; the SDK client
multiplexes by JSON-RPC id), saving per-server round-trips at startup.
- `@server:uri` resource reads run in parallel (`Promise.allSettled`) instead
of serially, matching how the file path batches via `readManyFiles`; order
is preserved so cards/labels line up with refs.
- Resource injection is now capped and framed: text is bounded by
`MAX_MCP_RESOURCE_TEXT_CHARS` (100k) and oversized blobs are skipped, so a
misbehaving/hostile server can't overflow the context window or OOM; the
content is fenced with `--- Content from MCP resource <label> ---` /
`--- End ---` delimiters so the model can separate untrusted server output
from the user's prompt. The success card reports truncation.
- File-read error path now merges `resourceLabels` into `filesRead` /
recording, so a resource read that succeeded before a file read failed is
not dropped from the audit trail.
- `isMethodNotFound` (JSON-RPC -32601) now also covers `discoverTools` and
`invokeMcpPrompt`, replacing the remaining message-substring checks.
- `PoolEntry.markActive` `initialResources` is now required (no `= []`
default), removing a footgun where an omitted arg would wipe a server's
resources via `applyResources([])`.
- `useAtCompletion` resource suggestions rank prefix matches above mid-string
matches.
- `'Resources:'` added to the remaining 6 locales (ca, de, fr, ja, pt, ru)
for parity with `'Prompts:'`.
Tests: attribution framing, text truncation, completion prefix ranking.
* test(mcp): cover resource registration in discover() and resource-only discovery
Closes the two coverage gaps flagged in review: assert discover() registers
discovered resources into the Config ResourceRegistry, and that a resource-only
server (no tools/prompts) is a successful discovery rather than throwing.
* fix(mcp): idempotent resource re-discovery + cumulative blob cap
Review round 4 (all Suggestions):
- discover() now clears a server's resources (removeResourcesByServer) before
re-registering, so reconnect / incremental re-discovery is idempotent and a
resource the server dropped doesn't linger in the registry (matches the
pool path's SessionMcpView.applyResources).
- @server:uri injection now caps CUMULATIVE blob size per resource, not just
each blob, so many sub-limit blobs in one response can't inject unbounded
data. Added a test for the oversized-blob skip + card.
- Documented that file/resource content parts are grouped by type (model
correlates by delimiter labels, not position).
* test(mcp): cover the MCP-status command reload (prompts surfacing in /)
Adds the missing coverage for the discovery-driven reload: a CONNECTED status
fires the listener and rebuilds the command tree (so progressively-discovered
MCP prompts appear as / commands), and a non-CONNECTED status does not.
* fix(mcp): don't wipe resources when resources/list transiently fails
- discover() only clears + replaces a server's resources when listMcpResources
returns a non-empty set. Because that helper swallows all errors (including
transient network failures) and returns [], an unconditional clear-then-
register would silently purge a server's resources on a transient list
failure while tools/prompts succeed. Guarding on length>0 keeps the existing
set on failure; a real partial drop still re-registers the fresh set.
- Resource success card now shows '(truncated)' for capped/skipped blobs too,
not just text. Added a cumulative-blob-cap test (two sub-limit blobs whose
sum exceeds the cap).
* fix(mcp): guard pool applyResources against transient-failure wipe too
The non-pool discover() guard (resources.length > 0) left the pool path
exposed: on a restart, doRestart -> discoverAndReturn swallows a transient
resources/list failure to [], and applyResources([]) then wiped the session's
resources. applyResources is now a no-op on an empty snapshot (mirrors the
discover() guard; applyTools/applyPrompts keep their pre-existing clear-on-empty
behavior, out of scope). Added tests: applyResources([]) does not clear, and
discover() with an empty resource list does not call removeResourcesByServer.
* fix(mcp): preserve pool resource snapshot on transient restart failure
The applyResources([]) no-op only protected already-attached sessions; doRestart
still overwrote the pool entry's resourcesSnapshot with [] when the restart's
resources/list transiently failed, so any session attaching AFTER the restart
got zero resources. doRestart now only updates resourcesSnapshot when the
re-read is non-empty, preserving it for new and existing subscribers alike.
Tests: applyResources([]) preserves a pre-populated set; a restart whose
resources/list comes back empty still serves the prior resource to a new
session.
* fix(mcp): trust-gate resource completion, colon server names, narrower method-not-found
- useAtCompletion no longer surfaces resource URIs in an untrusted folder
(the read path is already blocked there); avoids leaking resource existence.
- parseMcpResourceRef / getMcpResourceSuggestions match the LONGEST configured
server name as a '<name>:' prefix instead of splitting on the first colon,
so a server whose name contains ':' (a valid settings.json key) resolves.
- isMethodNotFound's message fallback is back to the case-sensitive exact
'Method not found' substring (the -32601 code is the primary check), not a
broad /method not found/i that would swallow unrelated errors.
Tests: @my:server:uri resolution, untrusted-folder completion.
* refactor(mcp): extract shared longest-prefix server matcher + doc/test fixes
- Extract matchMcpServerPrefix (new mcpResourceRef.ts) and use it from both
parseMcpResourceRef (injection) and getMcpResourceSuggestions (completion),
removing the duplicated longest-prefix logic and its drift risk.
- Update parseMcpResourceRef JSDoc to describe longest-prefix matching.
- Tests: shared-helper unit tests; the @my:server colon test now configures
both 'my' and 'my:server' to exercise disambiguation; a colon completion
test; isMethodNotFound message-casing tests (exact 'Method not found'
swallowed, 'method not found handler' not swallowed).
22 KiB
Connect Qwen Code to tools via MCP
Qwen Code can connect to external tools and data sources through the Model Context Protocol (MCP). MCP servers give Qwen Code access to your tools, databases, and APIs.
What you can do with MCP
With MCP servers connected, you can ask Qwen Code to:
- Work with files and repos (read/search/write, depending on the tools you enable)
- Query databases (schema inspection, queries, reporting)
- Integrate internal services (wrap your APIs as MCP tools)
- Automate workflows (repeatable tasks exposed as tools/prompts)
Tip
If you’re looking for the “one command to get started”, jump to Quick start.
Quick start
Qwen Code loads MCP servers from mcpServers in your settings.json. You can configure servers either:
- By editing
settings.jsondirectly - By using
qwen mcpcommands (see CLI reference)
Add your first server
- Add a server (example: remote HTTP MCP server):
qwen mcp add --transport http my-server http://localhost:3000/mcp
- Open MCP management dialog to view and manage servers:
qwen mcp
- Restart Qwen Code in the same project (or start it if it wasn’t running yet), then ask the model to use tools from that server.
Where configuration is stored (scopes)
Most users only need these two scopes:
- Project scope (default):
.qwen/settings.jsonin your project root - User scope:
~/.qwen/settings.jsonacross all projects on your machine
Write to user scope:
qwen mcp add --scope user --transport http my-server http://localhost:3000/mcp
Tip
For advanced configuration layers (system defaults/system settings and precedence rules), see Settings.
Configure servers
Choose a transport
| Transport | When to use | JSON field(s) |
|---|---|---|
http |
Recommended for remote services; works well for cloud MCP servers | httpUrl (+ optional headers) |
sse |
Legacy/deprecated servers that only support Server-Sent Events | url (+ optional headers) |
stdio |
Local process (scripts, CLIs, Docker) on your machine | command, args (+ optional cwd, env) |
Note
If a server supports both, prefer HTTP over SSE.
Configure via settings.json vs qwen mcp add
Both approaches produce the same mcpServers entries in your settings.json—use whichever you prefer.
Stdio server (local process)
JSON (.qwen/settings.json):
{
"mcpServers": {
"pythonTools": {
"command": "python",
"args": ["-m", "my_mcp_server", "--port", "8080"],
"cwd": "./mcp-servers/python",
"env": {
"DATABASE_URL": "$DB_CONNECTION_STRING",
"API_KEY": "${EXTERNAL_API_KEY}"
},
"timeout": 15000
}
}
}
CLI (writes to project scope by default):
qwen mcp add pythonTools -e DATABASE_URL=$DB_CONNECTION_STRING -e API_KEY=$EXTERNAL_API_KEY \
--timeout 15000 python -m my_mcp_server --port 8080
HTTP server (remote streamable HTTP)
JSON:
{
"mcpServers": {
"httpServerWithAuth": {
"httpUrl": "http://localhost:3000/mcp",
"headers": {
"Authorization": "Bearer your-api-token"
},
"timeout": 5000
}
}
}
CLI:
qwen mcp add --transport http httpServerWithAuth http://localhost:3000/mcp \
--header "Authorization: Bearer your-api-token" --timeout 5000
SSE server (remote Server-Sent Events)
JSON:
{
"mcpServers": {
"sseServer": {
"url": "http://localhost:8080/sse",
"timeout": 30000
}
}
}
CLI:
qwen mcp add --transport sse sseServer http://localhost:8080/sse --timeout 30000
Using MCP prompts and resources
Besides tools, Qwen Code discovers and surfaces two other MCP primitives.
Prompts (slash commands)
Any prompt a server advertises via prompts/list becomes an executable
slash command. After discovery, type / and you'll see the prompt
listed (labeled MCP: <server>); run it like any other command:
/my_prompt --arg1="value" --arg2="value"
# positional form also works:
/my_prompt "value" "value"
# show the prompt's arguments:
/my_prompt help
The prompt's messages are sent to the model, which then acts on them.
Discovery is lenient about the declared
promptscapability: some servers implementprompts/listbut omitpromptsfrom theirinitializecapabilities. Qwen Code attemptsprompts/listanyway, so those prompts still appear. A server that genuinely has no prompts simply answersMethod not found, which is ignored.
Resources
Resources a server advertises via resources/list are discovered per
server. Open the management dialog with /mcp and select a server to see
its Resources count alongside its tools and prompts. As with prompts,
the resources capability is not required to be declared.
Inject a resource's contents into your message with the @server:uri
syntax — type @, then the server name, a colon, and the resource URI:
summarize @myserver:file:///docs/spec.md and list the open questions
Typing @myserver: shows an autocomplete list of that server's resource
URIs. On submit, the referenced resource is read and its contents are
appended to your message (text inline, binary blobs as attachments); the
@server:uri reference is preserved in the prompt so the model knows what
it is looking at. The server prefix must match a configured MCP server —
otherwise the token is treated as a normal file path, so existing
@path/to/file references are unaffected. Resource reads are disabled in
untrusted folders.
Progressive availability and discovery timeouts
Qwen Code discovers MCP servers in the background after the UI is already interactive. You see the cli's first prompt within a few hundred milliseconds even when one of your MCP servers takes several seconds (or never responds), and the model's tool list updates within roughly one frame (~16 ms) of each server completing its discover handshake.
- Interactive mode: the UI appears immediately; an MCP status pill in
the bottom-right shows
N/M MCP servers readywhile discovery is in flight. Sending a prompt before MCP finishes simply means the model sees the tools that are ready at that moment; subsequent prompts see more tools as servers come online. - Non-interactive mode (
--prompt, stream-json, ACP): the cli still waits for MCP discovery to settle before sending the first prompt, so scripted / piped invocations see the same complete tool set the legacy synchronous behavior produced.
Per-server discoveryTimeoutMs
Each MCP server gets a discovery-only timeout that caps how long the
initial handshake (connect + tools/list + prompts/list +
resources/list) is allowed to take. Defaults:
- stdio servers: 30 s
- remote HTTP / SSE servers: 5 s (network risk is higher)
Override per server when needed:
{
"mcpServers": {
"slow-stdio": {
"command": "node",
"args": ["./slow-server.js"],
"discoveryTimeoutMs": 60000,
},
"flaky-remote": {
"httpUrl": "https://example.com/mcp",
"discoveryTimeoutMs": 10000,
},
},
}
The existing timeout field is tool-call timeout (used for each
tools/call request, default 10 minutes) and is unaffected by
discoveryTimeoutMs — a long-running tool invocation is not a startup
pathology.
Rolling back progressive MCP
If you need the old synchronous behavior (cli waits for every MCP server
before showing any UI), set QWEN_CODE_LEGACY_MCP_BLOCKING=1 in your
environment. This is kept as an escape hatch for at least one release.
Safety and control
Trust (skip confirmations)
- Server trust (
trust: true): bypasses confirmation prompts for that server (use sparingly).
OAuth authentication
Qwen Code supports OAuth 2.0 authentication for MCP servers. This is useful when accessing remote servers that require authentication.
Basic usage
When you add an MCP server with OAuth credentials, Qwen Code will automatically handle the authentication flow:
qwen mcp add --transport sse oauth-server https://api.example.com/sse/ \
--oauth-client-id your-client-id \
--oauth-redirect-uri https://your-server.com/oauth/callback \
--oauth-authorization-url https://provider.example.com/authorize \
--oauth-token-url https://provider.example.com/token
Important: Redirect URI configuration
The OAuth flow requires a redirect URI where the authorization provider sends the authentication code.
-
Local development: By default, Qwen Code uses
http://localhost:7777/oauth/callback. This works when running Qwen Code on your local machine with a local browser. -
Remote/cloud deployments: When running Qwen Code on remote servers, cloud IDEs, or web terminals, the default
localhostredirect will NOT work. You MUST configure--oauth-redirect-urito point to a publicly accessible URL that can receive the OAuth callback.
Example for remote servers:
qwen mcp add --transport sse remote-server https://api.example.com/sse/ \
--oauth-redirect-uri https://your-remote-server.example.com/oauth/callback
Manual configuration via settings.json
You can also configure OAuth by editing settings.json directly:
{
"mcpServers": {
"oauthServer": {
"url": "https://api.example.com/sse/",
"oauth": {
"enabled": true,
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
"authorizationUrl": "https://provider.example.com/authorize",
"tokenUrl": "https://provider.example.com/token",
"redirectUri": "https://your-server.com/oauth/callback",
"scopes": ["read", "write"]
}
}
}
}
OAuth configuration properties:
| Property | Description |
|---|---|
enabled |
Enable OAuth for this server (boolean) |
clientId |
OAuth client identifier (string, optional with dynamic registration) |
clientSecret |
OAuth client secret (string, optional for public clients) |
authorizationUrl |
OAuth authorization endpoint (string, auto-discovered if omitted) |
tokenUrl |
OAuth token endpoint (string, auto-discovered if omitted) |
scopes |
Required OAuth scopes (array of strings) |
redirectUri |
Custom redirect URI (string). Critical for remote deployments. Defaults to http://localhost:7777/oauth/callback |
tokenParamName |
Query parameter name for tokens in SSE URLs (string) |
audiences |
Audiences the token is valid for (array of strings) |
Token management
OAuth tokens are automatically:
- Stored securely in
~/.qwen/mcp-oauth-tokens-v2.json(AES-256-GCM encrypted), with keychain storage preferred when available - Refreshed when expired (if refresh tokens are available)
- Validated before each connection attempt
Use the /mcp auth command within Qwen Code to manage OAuth authentication interactively.
Tool filtering (allow/deny tools per server)
Use includeTools / excludeTools to restrict tools exposed by a server (from Qwen Code’s perspective).
Example: include only a few tools:
{
"mcpServers": {
"filteredServer": {
"command": "python",
"args": ["-m", "my_mcp_server"],
"includeTools": ["safe_tool", "file_reader", "data_processor"],
"timeout": 30000
}
}
}
Global allow/deny lists
The mcp object in your settings.json defines global rules for all MCP servers:
mcp.allowed: allow-list of MCP server names (keys inmcpServers)mcp.excluded: deny-list of MCP server names
Example:
{
"mcp": {
"allowed": ["my-trusted-server"],
"excluded": ["experimental-server"]
}
}
Troubleshooting
- Server shows “Disconnected” in
qwen mcp list: verify the URL/command is correct, then increasetimeout. - Stdio server fails to start: use an absolute
commandpath, and double-checkcwd/env. - Environment variables in JSON don’t resolve: ensure they exist in the environment where Qwen Code runs (shell vs GUI app environments can differ).
Reference
settings.json structure
Server-specific configuration (mcpServers)
Add an mcpServers object to your settings.json file:
// ... file contains other config objects
{
"mcpServers": {
"serverName": {
"command": "path/to/server",
"args": ["--arg1", "value1"],
"env": {
"API_KEY": "$MY_API_TOKEN"
},
"cwd": "./server-directory",
"timeout": 30000,
"trust": false
}
}
}
Configuration properties:
Required (one of the following):
| Property | Description |
|---|---|
command |
Path to the executable for Stdio transport |
url |
SSE endpoint URL (e.g., "http://localhost:8080/sse") |
httpUrl |
HTTP streaming endpoint URL |
Optional:
| Property | Type/Default | Description |
|---|---|---|
args |
array | Command-line arguments for Stdio transport |
headers |
object | Custom HTTP headers when using url or httpUrl |
env |
object | Environment variables for the server process. Values can reference environment variables using $VAR_NAME or ${VAR_NAME} syntax |
cwd |
string | Working directory for Stdio transport |
timeout |
number (default: 600,000) |
Request timeout in milliseconds (default: 600,000ms = 10 minutes) |
trust |
boolean (default: false) |
When true, bypasses all tool call confirmations for this server (default: false) |
includeTools |
array | List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (allowlist behavior). If not specified, all tools from the server are enabled by default. |
excludeTools |
array | List of tool names to exclude from this MCP server. Tools listed here will not be available to the model, even if they are exposed by the server. Note: excludeTools takes precedence over includeTools - if a tool is in both lists, it will be excluded. |
targetAudience |
string | The OAuth Client ID allowlisted on the IAP-protected application you are trying to access. Used with authProviderType: 'service_account_impersonation'. |
targetServiceAccount |
string | The email address of the Google Cloud Service Account to impersonate. Used with authProviderType: 'service_account_impersonation'. |
Manage MCP servers with qwen mcp
You can always configure MCP servers by manually editing settings.json, but the CLI is usually faster.
Adding a server (qwen mcp add)
qwen mcp add [options] <name> <commandOrUrl> [args...]
| Argument/Option | Description | Default | Example |
|---|---|---|---|
<name> |
A unique name for the server. | — | example-server |
<commandOrUrl> |
The command to execute (for stdio) or the URL (for http/sse). |
— | /usr/bin/python or http://localhost:8 |
[args...] |
Optional arguments for a stdio command. |
— | --port 5000 |
-s, --scope |
Configuration scope (user or project). | project |
-s user |
-t, --transport |
Transport type (stdio, sse, http). |
stdio |
-t sse |
-e, --env |
Set environment variables. | — | -e KEY=value |
-H, --header |
Set HTTP headers for SSE and HTTP transports. | — | -H "X-Api-Key: abc123" |
--timeout |
Set connection timeout in milliseconds. | — | --timeout 30000 |
--trust |
Trust the server (bypass all tool call confirmation prompts). | — (false) |
--trust |
--description |
Set the description for the server. | — | --description "Local tools" |
--include-tools |
A comma-separated list of tools to include. | all tools included | --include-tools mytool,othertool |
--exclude-tools |
A comma-separated list of tools to exclude. | none | --exclude-tools mytool |
--oauth-client-id |
OAuth client ID for MCP server authentication. | — | --oauth-client-id your-client-id |
--oauth-client-secret |
OAuth client secret for MCP server authentication. | — | --oauth-client-secret your-client-secret |
--oauth-redirect-uri |
OAuth redirect URI for authentication callback. | http://localhost:7777/oauth/callback |
--oauth-redirect-uri https://your-server.com/oauth/callback |
--oauth-authorization-url |
OAuth authorization URL. | — | --oauth-authorization-url https://provider.example.com/authorize |
--oauth-token-url |
OAuth token URL. | — | --oauth-token-url https://provider.example.com/token |
--oauth-scopes |
OAuth scopes (comma-separated). | — | --oauth-scopes scope1,scope2 |
--oauth-*flags apply only to--transport sseand--transport http. Combining them with--transport stdiois rejected.
Removing a server (qwen mcp remove)
qwen mcp remove <name>