feat(mcp): open an agent's tools and skills rosters, so webchat can remove as well as add - #2132
Conversation
…emove as well as add `installSkill` and `installMcpServer` could only ever add: there was no way from webchat to disable a skill, remove one, or detach an MCP server, and the answer was to send the user to the Console by hand. Removal is a per-row decision over live state — naming the row to take away in a tool argument would mean the model guessing at rows it has never seen. So the roster itself is the surface. `manageAgentTools` opens `ui://agentconnect/agent-tools`, which mounts the same two cards the Console's Tools & Skills tab mounts, each row keeping its own add and remove control, under the reader's own JWT. `focus` narrows it to one roster. Each row saves itself as it is toggled and Done reports the resulting counts, never a server url or a repository address. Both installers' descriptions now point at it for the half of the job they do not do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
One correctness issue in the new roster dialog needs fixing: Done can complete while a row save is still pending, sending stale counts to the agent and hiding any subsequent save error. Coordinate completion with both cards’ pending writes before reading and reporting the final state.
The tool registration, focus routing, and existing Console permission checks otherwise look consistent. Reviewed the exact GitHub revision 6bc23cd59b8a761f1d809e22fd4e1d3c672cbc39; tests were not rerun locally because no trusted PR checkout was available.
sent by review-bot (Codex · gpt-6-astra) · open in session
| const done = async () => { | ||
| if (busy) return | ||
| setBusy(true) | ||
| setError('') | ||
| try { | ||
| const current = await fetchAgentDto(agent.id) |
There was a problem hiding this comment.
[P2] Wait for pending row saves before completing the dialog
busy only tracks this Done handler; both roster cards keep their asynchronous updateAgent writes in private saving state and remove rows optimistically. Remove a row and click Done while its PATCH is still pending: this GET can return the old roster, after which onCompleted notifies the agent and onClose unmounts the cards. A later successful write makes the reported counts stale, and a later failed write loses the card’s error message entirely. Expose the cards’ pending-save state to this dialog and wait for those writes before completing; also prevent new row edits while the final snapshot is being read.
…tled Each row in the Tools & Skills rosters saves itself the moment it is toggled, but Done read the agent and reported its counts without regard for a write still in flight — stale counts to the agent, and a card unmounted before it could show the error its save was about to produce. Both cards now report a pending write upward (`onBusyChange`), and the dialog disables Done and says "Saving…" until every one of them has settled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Fixed in 4a5ba99 — you were right, Done read the roster without regard for a write still in flight, which meant stale counts and a card unmounted before it could show the error its save was about to produce.
|
There was a problem hiding this comment.
The pending-save callbacks fix clicking Done after a row write starts. One part of the same P2 issue remains: row edits are still enabled while Done fetches the final counts. An edit during that read can leave the dialog reporting stale state and closing over a pending write. Disable row mutations for the duration of completion.
No other blocking findings in revision 4a5ba99be598734ffcc45989129601b31106390c. Reviewed through revision-addressed GitHub reads; tests were not rerun locally.
sent by review-bot (Codex · gpt-6-astra) · open in session
| canEdit={canEdit} | ||
| onBusyChange={mcpBusy} | ||
| /> | ||
| )} | ||
| {focus !== 'mcp' && <AgentSkillsCard agentId={agent.id} canEdit={canEdit} onBusyChange={skillsBusy} />} |
There was a problem hiding this comment.
[P2] Disable row edits while Done reads the final state
Both cards still receive canEdit={canEdit} while the dialog's busy is true. Click Done with no saves pending, then remove a row while fetchAgentDto is awaiting its response: the row starts a PATCH, but the already-running Done handler still reports the earlier snapshot and closes as soon as its GET resolves. Updating rowSaving does not stop that handler, so stale counts and hidden late save errors remain possible. This is the remaining direction of the original race; disable both cards' mutation controls while the completion read is in progress.
|
That fixes the save → Done case. My latest review of Disable both cards’ mutation controls while the dialog’s sent by |
Holding Done until pending writes settled left the other half of the window open: the rows stayed interactive while Done fetched the agent back, so an edit accepted during that read would be reported as part of a state it was not in. The cards now take `canEdit` false for the duration of the read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Fixed in ebb32d9 — right, holding Done only closed half the window. The cards now take |
There was a problem hiding this comment.
Reviewed revision ebb32d900c40c09f5196f595ae8c472015bb7468. The completion race is resolved: Done is disabled while either card saves, and both cards’ mutation controls are disabled while Done reads the final counts. No remaining blocking findings.
Review used revision-addressed GitHub reads. Tests were not rerun locally; CI was still running at review time.
sent by review-bot (Codex · gpt-6-astra) · open in session
|
Confirmed in sent by |
Follow-up to #2129, from using it:
installSkillandinstallMcpServercan only ever add.Asked to remove a skill from an agent, admin MCP had nothing to offer and had to send the user
to the Console by hand — install without uninstall is not a finished surface.
Why the roster, and not a
removeSkilltoolRemoval is a per-row decision over live state. A tool argument naming what to take away would
mean the model guessing at rows it has never seen — the wrong shape, and the same reason the
code-host surface is a dialog rather than a set of write tools. So the roster itself is the tool.
manageAgentToolsopensui://agentconnect/agent-tools, which mounts the same two cards theConsole's Tools & Skills tab mounts —
AgentToolsCard(attached MCP servers) andAgentSkillsCard(enabled skills) — each row keeping its own add and remove control, under thereader's own Console JWT.
focus: 'mcp' | 'skills'narrows it to one roster; omitted shows both,as the tab does. The card title follows the resource and the focus: Tools & skills, Skills,
MCP servers.
Because the two surfaces are the tab's own cards, they cannot drift from it, and every change
saves itself the moment a row is toggled. Done reports the resulting state as counts only —
never a server url or a repository address. Both installers' descriptions now name this tool for
the half of the job they do not do.
Verification
pnpm typecheck,pnpm lint, prettier — clean.mcp.route.test.tsintegration (35, real Postgres), daemon MCP suites, andthe web console modal suites — all green.
removeargument or an unknownfocus,the card title and tool name per focus, the resource in
resources/list, dialog routing, bothrosters mounting with the right props, the focus narrowing, the counts summary firing only on
Done, and a read-only reader still seeing the rosters.
🤖 Generated with Claude Code