You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(serve): add MCP server restart route with budget guard (#4175 Wave 4 PR 17)
Adds POST /workspace/mcp/:server/restart — strict-gated mutation
route that performs a single-server MCP restart through the ACP
child's `McpClientManager.discoverMcpToolsForServer`. Pre-checks the
live budget snapshot from PR 14 v1 (#4247) so a restart on a
budget-saturated workspace returns a soft refusal rather than
triggering a `BudgetExhaustedError` cascade through the discovery
loop.
Decision logic (ACP-side, in `qwen/control/workspace/mcp/restart`
extMethod):
- Server not in `getMcpServers()` → JSON-RPC `resourceNotFound` →
HTTP 404
- Server in `excludedMcpServers` → 200 with `{skipped:true,
reason:'disabled'}`
- `manager.isServerDiscovering(name)` → 200 with `{reason:'in_flight'}`
- Mode is `enforce`, server not in `reservedSlots`, total ≥ budget →
200 with `{reason:'budget_would_exceed'}`
- Otherwise: `discoverMcpToolsForServer(name, config)`, return
`{restarted:true, durationMs}`
Soft refusals still return 200 because the route understood the
request and reached a deterministic answer about why no restart
happened. Only hard "we cannot answer" cases (unknown server, no
live ACP child) escalate to non-2xx. This mirrors PR 14 v1's
discovery-time refusal contract: refusals don't throw, they get
recorded.
Bridge:
- New `restartMcpServer(serverName, originatorClientId)` forwards
through the new `SERVE_CONTROL_EXT_METHODS.workspaceMcpRestart`
extMethod against the live `liveChannelInfo()` channel
- Throws `SessionNotFoundError` (mapped to HTTP 404) when no ACP
child is alive — restart inherently requires a live
`McpClientManager` instance
- Fan-outs `mcp_server_restarted` (success) or
`mcp_server_restart_refused` (skip) to every live session SSE bus
Core:
- New public `McpClientManager.isServerDiscovering(serverName):
boolean` — reads `serverDiscoveryPromises.has(name)` so the
daemon can short-circuit a redundant restart with
`skipped:in_flight` instead of awaiting the original discovery
promise (HTTP latency stays bounded)
SDK additions:
- `DaemonClient.restartMcpServer(serverName, clientId?)` with
URL-encoded server name
- `DaemonMcpRestartResult` discriminated union, two new typed
events (`DaemonMcpServerRestartedEvent`,
`DaemonMcpServerRestartRefusedEvent`) with runtime guards,
reducer integration on `DaemonSessionViewState`
(`mcpRestartCount` / `lastMcpRestart` /
`mcpRestartRefusedCount` / `lastMcpRestartRefused`)
New capability tag `workspace_mcp_restart` (always-on, since v1).
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
0 commit comments