feat(desktop,web): an MCP button in the coding agent's composer switches each server for that chat
What
Dean: "We should add an MCP logo button in the composer after the model and before context window. Clicking on it should show the state of the MCP servers we have and we should also include a switch for each row to enable/disable for this chat."
Builds on integrations #2319 (provider-keyed integrations) and #2328 (Settings › Integrations, useIntegrationStatuses, IntegrationMark), both merged.
-
Button. The coding agent's composer foot now reads
[+] … [model ▾] [MCP] [context ring] [mic] [Send]. The MCP mark is an icon button with the tooltip "MCP servers" and no count badge: with one to four servers, a number reads as noise. The mark is the official MCP geometry (its three strokes, from modelcontextprotocol.io's own SVG), redrawn as aMcpIconglyph in our 1.9-stroke style andcurrentColor. It is not the project's artwork file, so no asset licence is involved. -
Popover. It hangs from the button's trailing edge like the model menu and context popover (
AnchoredPopover,align: END), on the menu classes #2301's tokens apply to. Each row is one integration provider (no built-in eve MCP servers exist; today Linear, Notion, and Sentry), with its mark, its name, and muted words:- Connected: a switch. On means this agent gets the server's tools from its next turn.
- Not connected (also while a sign-in is pending) or Sign-in expired: the row's end says Connect in Settings, which closes the popover and opens Settings › Integrations (#2328).
- Not available: no connector on this deployment; a muted row with nothing to press.
- Error: the status read failed; Try again reads it again.
A quiet note says "Changes apply from the agent's next turn." Defaults: every connected server is on.
-
Per-agent state. A new additive, nullable column
coding_agent.disabled_servers jsonbholds the provider ids switched off. Null, like an empty list, switches none off, so a server connected later is on for every agent. A provider no longer offered is ignored on read, on the row and on the wire, so neither a retired provider nor a newer service breaks an older desktop's reads. The migration is a plainADD COLUMNwith no rewrite, so it needs no expand/contract split. The column is written byPATCH /api/agents/{id}with{ disabledServers }(the whole list, so two quick switches land as the later one), and by Start's new optionaldisabledServers, so a draft agent keeps its switches until its first message starts it. Desktop path: actCODING_AGENTS_SERVERS→ gatewaycodingAgents.servers→ host composer →HostedCodingAgentClient.switchServers. The summary answersdisabledServerswhen non-empty. -
How it reaches eve. A static
defineMcpClientConnectionhas no per-session switch, but eve 0.74'sdefineDynamic(eve/connections) resolves a connection atturn.started, andnulltakes it away for that turn. eve then tells the model at its next step: "No longer available, do not call: linear".coder/host.ts'sintegrationConnectionFornow wraps #2319's definition inswitchableConnection(server/hosted/coder-host/servers.ts).- Each turn admits the session, reads the agent's row, and answers the provider's definition (with
instanceKey= the account id, which eve requires on an authenticated dynamic connection) ornull. - A running turn keeps its tools.
- The worker's connection files take the same switch through
admitDelegated, which resolves to the parent's conversation, so a worker follows its parent agent. - Fails closed: a row that cannot be read answers
null, logging only the failure's kind (causeKind). A server the developer switched off never comes back on because of a passing store error, and the turn carries on without it. - The planning assistant's connections are untouched.
Screenshots
Rendered in Chrome from the real AgentComposer and the app's own built stylesheet (a throwaway harness over the renderer modules, with a stubbed act bridge):
| Light | Dark | |
|---|---|---|
| Connected | ![]() | ![]() |
| Sign-in expired, and a provider not available | ![]() | ![]() |
Tests
apps/web/tests/coder-servers.test.ts(real store and host on PGlite):- A turn is handed the server unless its agent switched it off. The worker follows its parent; another account's agent is unaffected; switching back on hands it again.
- A session admitted for no agent, or a row that could not be read, hands no server, and the log carries
SqlErrorand never the statement. - The dynamic connection resolves to
nullwhen off, and to the provider's own definition (its tool filter included) keyed by its account when on.
integration-agents.test.ts: the planning agents register each provider's static definition, and the coding agents' entries for every provider resolve atturn.started.coding-agents-app.test.ts: Start and PATCH write and answerdisabledServers; the list reads it back; another account's agent is not found; a malformed list is refused.coding-agent-store.test.ts: the column round-trips from Start and from update; another account's write writes nothing; a retired provider id left on a row reads as nothing switched off.agent-servers.test.ts(jsdom, through the act bridge):- The popover lists each state's words.
- Toggling switches the server off and on again.
- Not connected and expired open Settings and close the popover.
- Unavailable has nothing to press; a failed read shows Error and Try again re-reads.
- Escape returns focus to the button.
use-agent-servers.test.ts: a switch shows at once, and a refusal falls back.use-coding-agents.test.ts:- The switch sends the act for that agent. An agent's switches are sent one after another, each once the previous one answered, so the service writes them in the order made, and only the newest answer stands.
- A refusal toasts.
- The draft's switches go with its Start.
agent-composer.test.ts: the foot's order is nowmodel, MCP servers, Context usage, Dictate, Send/Stop.host-operator.test.ts: a Start carries the draft'sdisabledServersto the host.- Client, host-composer, and IPC tests for the new method; JSON Schema goldens re-recorded under
LUKE_UPDATE_FIXTURES=1. The diff is purely additive: the new optionaldisabledServerskey (an array of ≤32 strings of ≤64 chars) on the summary, the answers, and the Start request, plus two new goldens for the PATCH body and its params.
Evidence
- Platform-independent checks:
./scripts/check.shexit 0 on this branch, rebased on main after #2328. - macOS Electron verification (
./scripts/verify.sh): not run. This is a Linux cloud VM. CI could not verify the Mac UI. A Mac break that lands is caught at the release rehearsal on a tag; LUKE-159 is the manualverify.shpass.
🤖 Generated with Claude Code



