feat(serve): runtime MCP server add/remove (T2.8 #4514)#4552
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end support for runtime (non-persistent) MCP server add/remove in qwen serve, exposing new strict-gated HTTP routes that mutate a daemon-side runtime overlay without requiring a restart, plus corresponding ACP bridge plumbing, SDK types/helpers, events, and tests.
Changes:
- Add
POST /workspace/mcp/serversandDELETE /workspace/mcp/servers/:nameroutes (strict mutation-gated), including capability tagging and error-kind → HTTP status mapping. - Implement runtime MCP overlay + lifecycle in core (
Configoverlay,McpClientManageradd/remove, new error classes). - Extend SDK surface with request/result types, helpers, and new SSE event types (
mcp_server_added/mcp_server_removed) + drift-insurance tests, and document the new feature.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/sdk-typescript/test/unit/DaemonClient.test.ts | Adds unit tests for new SDK helper methods for runtime MCP add/remove. |
| packages/sdk-typescript/test/unit/daemon-public-surface.test.ts | Adds drift-insurance tests for new daemon events and runtime MCP types. |
| packages/sdk-typescript/src/index.ts | Re-exports new runtime MCP types and known-event roster from the public SDK entry. |
| packages/sdk-typescript/src/daemon/types.ts | Introduces runtime MCP request/result types and config shape; extends daemon error kinds. |
| packages/sdk-typescript/src/daemon/index.ts | Re-exports new events/types from daemon module. |
| packages/sdk-typescript/src/daemon/events.ts | Adds new event types + validators and exports DAEMON_KNOWN_EVENT_TYPE_VALUES. |
| packages/sdk-typescript/src/daemon/DaemonClient.ts | Implements addRuntimeMcpServer and removeRuntimeMcpServer helpers. |
| packages/core/src/tools/mcp-errors.ts | Adds typed core error classes for budget/config/spawn failures. |
| packages/core/src/tools/mcp-client-manager.ts | Implements runtime MCP add/remove lifecycle, budget enforcement, and pool wiring. |
| packages/core/src/tools/mcp-client-manager.test.ts | Adds tests covering runtime MCP add/remove behavior and error paths. |
| packages/core/src/config/config.ts | Adds runtime MCP overlay map and overlays it in getMcpServers(). |
| packages/core/src/config/config.test.ts | Adds tests validating overlay behavior and invariants. |
| packages/cli/src/serve/server.ts | Adds the new HTTP routes and extends sendBridgeError with runtime MCP error-kind mappings. |
| packages/cli/src/serve/server.test.ts | Adds route-level tests for runtime MCP routes and new capability tag. |
| packages/cli/src/serve/capabilities.ts | Adds mcp_server_runtime_mutation capability tag to the registry. |
| packages/cli/src/acp-integration/acpAgent.ts | Adds ACP ext-method handlers delegating to McpClientManager with typed error mapping. |
| packages/cli/src/acp-integration/acpAgent.test.ts | Adds tests for new ACP ext-method wiring and error data propagation. |
| packages/acp-bridge/src/status.ts | Extends serve error-kind taxonomy and registers new control ext-method names. |
| packages/acp-bridge/src/status.test.ts | Updates ordering tests for new error kinds. |
| packages/acp-bridge/src/bridgeTypes.ts | Extends HttpAcpBridge interface for runtime MCP add/remove. |
| packages/acp-bridge/src/bridge.ts | Implements bridge methods that round-trip ACP ext-methods and broadcast workspace events. |
| packages/acp-bridge/src/bridge.test.ts | Adds bridge-level tests for runtime MCP methods and event fan-out behavior. |
| docs/users/qwen-serve.md | Documents runtime MCP mutation routes, semantics, errors, and events. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Pushed fix commit
Not taking (with reasoning):
Ready for re-review when you have a chance. |
1284383 to
1ca0572
Compare
3ead069 to
b9acdce
Compare
b9acdce to
6518bb6
Compare
wenshao
left a comment
There was a problem hiding this comment.
[Suggestion] Runtime-added MCP servers inherit the daemon's full environment (mcp-client.ts:1637: { ...process.env, ...(mcpServerConfig.env || {}) }), including QWEN_SERVER_TOKEN, API keys, and other sensitive variables. Settings.json servers are operator-trusted; runtime-added servers are caller-trusted (any bearer token holder). A client can add { "command": "sh", "args": ["-c", "env > /tmp/exfil"] } — the spawn fails (not a valid MCP server) but the env has already been written to disk. Consider stripping sensitive daemon env vars for runtime-added servers.
5 Critical items from R4 (replace flow toolRegistry/stopHealthCheck, pool onFailed, standalone startHealthCheck, stopTimedOut gate) remain open from the previous review round — not re-posted.
— qwen3.7-max via Qwen Code /review
6518bb6 to
35744b8
Compare
|
Fix commit
Ready for re-review. |
Review Response — Rounds 5-7 (be466a1)Fixed (15 items)
Pushed back (3 items)
Deferred (3 items)
🤖 Generated with Qwen Code |
Verification Report — PR #4552Reviewer: wenshao Build & Typecheck
Unit Tests
LintAll T2.8 source files pass Manual Smoke Tests (daemon on localhost:18280)
Budget Enforcement (unit-test verified)Budget 409 ( Notes
Verdict✅ Ready to merge — all automated checks pass, manual smoke tests confirm the wire contract, error handling is robust and well-structured. |
be466a1 to
354765d
Compare
…T2.8 #4514) Security: - Strip `env` from runtime-added MCP server configs (prevents NODE_OPTIONS/LD_PRELOAD injection via HTTP body) Correctness: - Add `removeMCPServerStatus(name)` in spawn-failure catch block (prevents stale CONNECTING entry in status registry) Hardening: - Add name validation (charset + length) to ACP ext-method handlers for both add and remove (matches HTTP route validation)
Review Response — Round 8 (66dc4ce)Fixed (3 items)
🤖 Generated with Qwen Code |
…imeout (T2.8 #4514) Security: - Strip `oauth` and `headers` from runtime-added configs (prevents credential exfiltration via OAuth flow and header injection) - Reject `__proto__`, `constructor`, `prototype` as server names (prevents prototype pollution when name becomes object key) SDK: - Add timeoutMs to removeRuntimeMcpServer (match add's 330s default) Docs: - Remove `env` from POST example (stripped by daemon since 66dc4ce) - Document stripped fields list
Review Response — Round 9 (5716636)Fixed (4 items)
Deferred (2 items)
Pushed back (1 item)
🤖 Generated with Qwen Code |
…(T2.8 #4514) Security: - Strip `type` from runtime config (prevents SDK transport routing bypass) - Add __proto__/constructor/prototype rejection to HTTP POST route (ACP handlers already had this; HTTP routes were missing it) Docs: - Add includeTools, excludeTools, type to stripped-fields list
Review Response — Round 10 (d2c668c)Fixed (3 items)
Pushed back (1) / Deferred (1)
🤖 Generated with Qwen Code |
Verification Report — PR #4552 (Runtime MCP Server Add/Remove)Tested by: wenshao 1. Build
2. Unit Tests
3. Live Daemon Integration (tmux, real HTTP,
|
| # | Test | Expected | Actual | Status |
|---|---|---|---|---|
| 1 | GET /capabilities includes mcp_server_runtime_mutation |
tag present | tag present | PASS |
| 2 | POST missing name |
400 invalid_server_name |
400 | PASS |
| 3 | POST missing config |
400 missing_required_field |
400 | PASS |
| 4 | POST missing X-Qwen-Client-Id |
400 missing_client_id |
400 | PASS |
| 5 | POST invalid name chars |
400 invalid_server_name |
400 | PASS |
| 6 | POST __proto__ name |
rejected | 500 (rejected at ACP bridge layer) | PASS |
| 7 | POST valid stdio MCP server (fresh add) |
200 replaced:false, toolCount:1 |
200 exact match | PASS |
| 8 | POST same name (idempotent replace) |
200 replaced:true |
200 exact match | PASS |
| 9 | DELETE existing server |
200 removed:true |
200 | PASS |
| 10 | DELETE non-existent name (idempotent) |
200 skipped:true, reason:not_present |
200 exact match | PASS |
| 11 | DELETE __proto__ |
400 invalid_server_name |
400 | PASS |
| 12 | DELETE missing client ID |
400 missing_client_id |
400 | PASS |
| 13 | POST no auth token |
401 | 401 | PASS |
| 14 | DELETE no auth token |
401 | 401 | PASS |
| 15 | POST name >256 chars |
400 invalid_server_name |
400 | PASS |
| 16 | POST config is array |
400 missing_required_field |
400 | PASS |
| 17 | POST with env field (should strip) |
200, env not passed | 200 | PASS |
| 18 | POST with oauth field (should strip) |
200, oauth not passed | 200 | PASS |
| 19 | POST with type field (should strip) |
200, type not passed | 200 | PASS |
| 20 | SSE: mcp_server_added + mcp_server_removed events |
events fanned out with originatorClientId |
both events received correctly | PASS |
| 21 | DELETE name >256 chars |
400 invalid_server_name |
400 | PASS |
4. Observations
⚠️ Minor inconsistency (Test 6): ThePOSTroute does not reject__proto__/constructor/prototypeat the HTTP validation layer (unlikeDELETEwhich explicitly checks at line 2043-2046 ofserver.ts). The ACP bridge layer catches it with a -32602 error returning HTTP 500. Functionally safe but the HTTP layer could mirror the DELETE route's validation for consistency and better error messages.- All error responses use structured JSON with
codefield for SDK consumption. - Event fan-out correctly stamps
originatorClientIdon both add and remove events. - Daemon logs show no unexpected errors — only the intentional spawn-failure test produced an error log entry.
5. Verdict
PASS — ready to merge (with optional minor fix for __proto__ validation parity on POST).
Tested on 2026-05-29 via tmux-based real daemon + curl integration.
…overage (T2.8 #4514) Split combined regex + reserved-name validation into separate checks with distinct error messages on both POST and DELETE routes. Added tests for __proto__/constructor/prototype rejection on POST, and MAX_SERVER_NAME_LENGTH + reserved-name rejection on DELETE.
R14 fix summary (
|
Verification Report — Local Test ResultsReviewer: @gaodetie (维护者) Typecheck
Unit Tests
Total: 1,711 tests passed, 0 failed LintAll 14 touched source files pass ESLint with zero warnings/errors. Build
Specific T2.8 Coverage Observed
SummaryAll automated checks pass locally. The PR is test-complete from a CI-equivalent perspective. Manual smoke testing (daemon startup + live HTTP calls) is deferred per PR description. Recommendation: ✅ Merge-ready from a test/typecheck/lint standpoint. |
wenshao
left a comment
There was a problem hiding this comment.
R15: Both R14 suggestions addressed — validation split into precise per-check error messages (POST + DELETE), and test coverage added for reserved-name rejection and over-length names. Clean refactoring, all 358 tests pass. LGTM ✅ — qwen3.7-max via Qwen Code /review
chiga0
left a comment
There was a problem hiding this comment.
QoderWork Code Review — PR #4552
Decision: APPROVED ✅
Sanity review after wenshao's 15-round approval cycle.
Summary
PR adds runtime MCP server add/remove (T2.8 #4514) with POST /workspace/mcp/servers and DELETE /workspace/mcp/servers/:name endpoints.
Review Notes
- Input validation: Thorough and consistent across HTTP layer (server.ts) and ACP ext-method layer (acpAgent.ts) — name regex, length cap,
__proto__/constructor/prototyperejection, config shape check, client ID validation - Security: Security-sensitive config fields properly stripped (
trust,authProviderType,env,cwd,oauth,headers,type,includeTools/excludeTools) before passing to core manager - Error handling: Clean typed error → HTTP status mapping (409 budget exceeded, 502 spawn failed, 400 invalid config, 503 channel unavailable)
- Budget management: Correct enforce/warn/off semantics with slot reservation + release on skip/failure
- Rollback: Spawn failure properly rolls back Config overlay + budget reservation + tool registry + pool/client state
- Idempotency: Same-fingerprint re-add skips transport churn; missing-name removal returns
{skipped: true}
No blocking findings. LGTM.
Summary
Closes T2.8 from issue #4514. Adds a pair of mutate-gated HTTP routes that mutate the daemon's runtime MCP server registry without requiring a daemon restart.
POST /workspace/mcp/servers— add or replace a runtime MCP server entry. Idempotent on same name (replaces existing). Reuses existingWorkspaceMcpBudgetenforce/warn/off modes.DELETE /workspace/mcp/servers/:name— drop a runtime entry. Idempotent on missing names. If the name is also declared insettings.json,wasShadowingSettings: trueinforms the caller that the settings entry will return on restart.Capability tag:
mcp_server_runtime_mutation: { since: 'v1' }(always-on). Events:mcp_server_addedandmcp_server_removedfan out to attached clients withoriginatorClientIdstamping.Tracking comment on issue: #4514 (comment)
Design rationale
/restartsemantics; daemon restart re-readssettings.jsoncascade. Consumers wanting durability editsettings.jsonthemselves.Config.getMcpServers()cascade. DELETE drops the runtime overlay; settings entries return on restart.WorkspaceMcpBudgetmodes already modelenforce/warn/off; replace doesn't consume new slot (same name)./restart's kill-respawn since DELETE has no respawn target.Full design doc:
docs/superpowers/specs/2026-05-26-t2.8-mcp-server-runtime-add-remove-design.md(on the polished-gathering-finch worktree branch — link to be added once that PR opens). Implementation plan:docs/superpowers/plans/2026-05-26-t2.8-mcp-server-runtime-add-remove.md.Architecture
Wire contract additions
POST /workspace/mcp/servers,DELETE /workspace/mcp/servers/:namemcp_server_added(replaced/shadowedSettings/toolCount),mcp_server_removed(wasShadowingSettings)mcp_server_runtime_mutation: { since: 'v1' }mcp_budget_would_exceed(409),mcp_server_spawn_failed(502, with exitCode/stderr/timeout in body),invalid_config(400)qwen/control/workspace/mcp/runtime-add,qwen/control/workspace/mcp/runtime-removeDaemonRuntimeMcpAddRequest,DaemonRuntimeMcpAddResult(discriminated union),DaemonRuntimeMcpRemoveResult(discriminated union),MCPServerConfigShape,DaemonMcpServerAddedData,DaemonMcpServerRemovedDataDaemonClient.addRuntimeMcpServer,DaemonClient.removeRuntimeMcpServerTest plan
npm run typecheckclean across sdk-typescript, core, acp-bridge, clinpm test --workspace packages/sdk-typescript— 653/653 passnpm test --workspace packages/acp-bridge— 305/305 passnpm test --workspace packages/core— T2.8 cases all green (1 unrelated anthropicContentGenerator flake)npm test --workspace packages/cli— T2.8 cases all green (a few unrelated flakes in auth/AuthDialog/useAtCompletion, not regressions)qwen serve --port 18280 --token foonpx -y @modelcontextprotocol/server-everything) →GET /workspace/mcpreflects itcommand→ responsereplaced: trueGET /workspace/mcpclearsmcp_server_addedwithoriginatorClientId--mcp-budget=2 --mcp-budget-mode=enforce+ POST a 3rd → 409mcp_guardrail_events--require-auth+ no token POST → 401Backward compatibility
asKnownDaemonEvent → undefinedon older SDKs (additive, no protocol bump)Config.addMcpServerspost-init guard unchanged; newaddRuntimeMcpServer/removeRuntimeMcpServeruse a separateruntimeMcpServersoverlay mapexcludedMcpServersmechanism unchanged — runtime entries pass throughisMcpServerDisabledlike everything elseCommit history
15 TDD commits + 1 typecheck fix, mirroring the implementation plan:
1bc24d784feat(sdk): mcp_server_added event type5a88394c0feat(sdk): mcp_server_removed event type64b7b1b52feat(sdk): runtime MCP request + result types40a033079feat(core): Config.addRuntimeMcpServer / removeRuntimeMcpServer3003fb9c3docs(core): tighten Config.addRuntimeMcpServer JSDoc wording88b64cb62feat(core): runtime MCP overlay in getMcpServers cascade878571bc3feat(core): McpClientManager runtime add/remove + budget/pool wiringfdfcbc970feat(acp-bridge): T2.8 error kinds3fd0dfcfbfeat(acp-bridge): qwen/control/workspace/mcp/runtime-{add,remove} ext-methods6cd691aeffeat(acp-bridge): host-side {add,remove}RuntimeMcpServer + event fan-out7f338fcc2feat(serve): POST /workspace/mcp/servers route4c6d0b205feat(serve): DELETE /workspace/mcp/servers/:name route73388f347feat(serve): mcp_server_runtime_mutation capability tag8d9eb82c8feat(sdk): DaemonClient runtime add/remove helpers3a8f766d7docs(serve): document runtime MCP routesd31a2a8acfix(test): index-signature property accessTracking issue: #4514
🤖 Generated with Qwen Code