Skip to content

feat(mcp): open an agent's tools and skills rosters, so webchat can remove as well as add - #2132

Merged
spacedragon merged 3 commits into
mainfrom
dev/yulong/agent-tools-card
Sep 17, 2026
Merged

spacedragon merged 3 commits into
mainfrom
dev/yulong/agent-tools-card

Conversation

@spacedragon

Copy link
Copy Markdown
Contributor

Follow-up to #2129, from using it: installSkill and installMcpServer can 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 removeSkill tool

Removal 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.

manageAgentTools opens ui://agentconnect/agent-tools, which mounts the same two cards the
Console's Tools & Skills tab mounts
— AgentToolsCard (attached MCP servers) and
AgentSkillsCard (enabled skills) — each row keeping its own add and remove control, under the
reader'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.
  • control-plane unit + mcp.route.test.ts integration (35, real Postgres), daemon MCP suites, and
    the web console modal suites — all green.
  • New coverage: the tool's intent and its refusal of a remove argument or an unknown focus,
    the card title and tool name per focus, the resource in resources/list, dialog routing, both
    rosters 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

…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>

@agentconnect-md-test agentconnect-md-test Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +45 to +50
const done = async () => {
if (busy) return
setBusy(true)
setError('')
try {
const current = await fetchAgentDto(agent.id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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>
@spacedragon

Copy link
Copy Markdown
Contributor Author

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.

AgentToolsCard and AgentSkillsCard now report their pending write upward through an optional onBusyChange, and the dialog disables Done and shows "Saving…" until every card has settled. Covered by a new case: toggling a row into its saving state disables Done, and clicking it then reads nothing and reports nothing.

@agentconnect-md-test agentconnect-md-test Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +83 to +87
canEdit={canEdit}
onBusyChange={mcpBusy}
/>
)}
{focus !== 'mcp' && <AgentSkillsCard agentId={agent.id} canEdit={canEdit} onBusyChange={skillsBusy} />}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@agentconnect-md-test

Copy link
Copy Markdown
Contributor

That fixes the save → Done case. My latest review of 4a5ba99b flags the reverse ordering: click Done, then edit a row while its GET is pending. Both cards remain editable, and Done can close before that new save settles.

Disable both cards’ mutation controls while the dialog’s busy state is true to complete the fix.

sent by review-bot (Codex · gpt-6-astra) · open in session

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>
@spacedragon

Copy link
Copy Markdown
Contributor Author

Fixed in ebb32d9 — right, holding Done only closed half the window. The cards now take canEdit false for the duration of the completion read, so no edit can be accepted while Done is fetching the state it will report. The read-only notice still keys off the agent’s real canEdit, so it does not flash during a normal completion. New case covers it: with the read left pending, the skills card is mounted with canEdit: false.

@agentconnect-md-test agentconnect-md-test Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@agentconnect-md-test

Copy link
Copy Markdown
Contributor

Confirmed in ebb32d90: both cards disable edits during the completion read, and the read-only notice still reflects actual permissions. Approved that revision; no remaining blocking findings.

sent by review-bot (Codex · gpt-6-astra) · open in session

@spacedragon
spacedragon merged commit a2d70b1 into main Sep 17, 2026
2 checks passed
@spacedragon
spacedragon deleted the dev/yulong/agent-tools-card branch September 17, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant