feat(mcp): open the agent editor, skill installer and MCP installer as native apps - #2129
Conversation
…s native apps Admin MCP could only open the integration and code-host surfaces. The agent editor, the skills library installer and "Add MCP server" now get the same treatment, so a configuration the model cannot safely perform is handed to the browser instead of being asked for in chat. - protocol: `agent-setup`, `skill-setup` and `mcp-setup` intents, plus one shared `nativeUiTitle` so the daemon's card chrome and the Console card cannot word the same surface differently. - control-plane: `configureAgent`, `installSkill` and `installMcpServer` — read-only opens that validate the org and the named agent with the caller's own credential. `createAgent` keeps its own answer and carries the new agent's editor beside it under `nativeUi`, so a creation's first card is that agent's configuration. - daemon: an intent is now accepted beside a write tool's answer as well as as the answer. - web: the three dialogs mount the Console's existing editors under the reader's own JWT. An installer given an agentId also enables the source or attaches the server on it, and reports that second write separately — it can fail on its own. No credential, url or secret rides in an intent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Two functional issues need fixing in the new agent-editor flow:
- The normal Webchat create → approve → getOperation path never projects the new “Agent created” card, because the intent remains inside the serialized operation result.
- Opening configuration immediately after creation can show “This agent is unavailable” until the Console’s next 30-second agent-list refresh.
Reviewed the exact base and head (be1726dd8520d5bb5a3d05ac9d1bbb077c9f5324) through GitHub read-only inspection. Local tests were not run because no trusted PR checkout was available.
sent by review-bot (Codex · gpt-6-astra) · open in session
| function intentIn(value: unknown): NativeMcpUi | undefined { | ||
| const direct = NativeMcpUi.safeParse(value) | ||
| if (direct.success) return direct.data | ||
| const beside = NativeMcpUi.safeParse(record(value)?.nativeUi) | ||
| return beside.success ? beside.data : undefined |
There was a problem hiding this comment.
[P2] Surface the creation intent after delegated approval
The built-in Webchat admin calls createAgent with an invocationContext, so mcp/routes.ts returns a 202 operation without executing this tool. Browser approval executes it later and stores { statusCode, body }; getOperation returns that under result, with body still a JSON string containing the new nativeUi. This extractor accepts only a direct intent or a top-level nativeUi, while the approval UI only sends a status notice. Consequently, the normal create → approve → getOperation flow never produces the advertised “Agent created” editor card. Surface the completed creation intent through getOperation or the approval-completion path, while keeping pending operations card-free.
There was a problem hiding this comment.
Fixed in cc36a7f. You were right: the executed tool's answer is a JSON string inside the bounded envelope, so the intent was invisible to every reader of the operation. getOperation's DTO now lifts a valid nativeUi out of that string onto the operation itself (webchat-mcp-operations.ts), and the field is declared on OperationDto so serialization keeps it. A pending operation has no bounded result and therefore still no card — asserted in the delegated approve test in mcp.route.test.ts.
There was a problem hiding this comment.
The DTO hoist fixes the nesting issue at cc36a7fd, and pending operations correctly carry no intent.
One gap remains: getOperation returns text only, and the daemon parser skips blocks over 4,096 characters. A valid creation with a 5,000-character description still produces no card after approval.
Please expose the compact intent through structuredContent or a separate bounded text block so card detection does not depend on the full operation-result size.
sent by review-bot (Codex · gpt-6-astra) · open in session
There was a problem hiding this comment.
Good catch — fixed in 93cab23. Rather than special-casing getOperation, the MCP route now republishes any result body that carries a nativeUi beside its own answer as structuredContent, so card detection never depends on the text block’'s length. That covers the operation read and every future write tool that earns a card. Asserted in the delegated approve test: structuredContent.nativeUi equals the intent on the operation DTO.
There was a problem hiding this comment.
Confirmed in 93cab231: the daemon reads structuredContent.nativeUi before the text-size check. This resolves the blocking finding, and I’ve approved the revision. The text-only adapter caveat remains non-blocking.
sent by review-bot (Codex · gpt-6-astra) · open in session
| if (loading) return notice('Loading configuration…') | ||
| const agent = agents.find((item) => item.id === ui.intent.agentId) | ||
| if (!agent) return notice('This agent is unavailable.') |
There was a problem hiding this comment.
[P2] Refresh the requested agent before declaring it unavailable
If configureAgent opens immediately after an MCP creation, the Console can have loading === false while its cached agents list still predates that creation. This branch then shows “This agent is unavailable” for an agent that exists and is editable. MCP creation does not invoke the browser’s createAgent cache invalidation, and opening this dialog performs no refresh; the list only polls every 30 seconds. This also affects the new creation card whenever it reaches the browser. Fetch the requested agent or revalidate the roster on opening, and wait for that read before treating a missing cached row as unavailable.
There was a problem hiding this comment.
Fixed in cc36a7f. The dialog now revalidates the roster once when the requested agent is not in the cached list, and shows "Loading configuration…" until that read comes back — only an empty result after it is reported as unavailable. The ask is guarded by a ref so a remount does not re-fetch. Covered by a new case in NativeSetupDialogs.test.tsx.
There was a problem hiding this comment.
The original 30-second stale-cache delay is addressed.
One non-blocking detail remains: refresh() returns void, so Promise.resolve(refresh()).finally(...) immediately marks revalidation complete. The dialog can therefore show “unavailable” while the request is still running. Return and await the actual refresh promise to preserve the loading state; the test’s promise-returning mock currently hides this mismatch.
sent by review-bot (Codex · gpt-6-astra) · open in session
…ld agent roster Two gaps the review found in the new agent-editor flow. A delegated `createAgent` does not execute in its own request, and the executed tool's answer is stored as a JSON string inside the bounded operation envelope — so the intent was invisible to every reader of that operation, and the normal create → approve → getOperation path never produced the card. `getOperation` now lifts a valid intent out of that string onto the operation itself; a pending operation still has no result and no card. The agent editor also declared a just-created agent unavailable whenever the browser's cached roster predated the MCP creation, which it does until the next poll. It now revalidates the roster once and waits for that read before saying so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`getOperation` answered in text only, and the daemon's parser skips a text block over 4,096 characters — so an approved creation whose agent carries a long description produced no card. Any result body that carries a `nativeUi` beside its own answer is now republished as structured content, which no size ceiling applies to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed cc36a7fdb4f3e5f62d292139030db7db31fd64e8. The approval-result nesting is fixed, but one functional issue remains: getOperation returns the intent only inside its full text result, which the daemon skips when it exceeds 4,096 characters. A valid creation with a long description therefore still gets no editor card after approval.
Non-blocking: the editor now requests a roster refresh, but marks it complete before the request finishes, briefly showing an incorrect “unavailable” notice.
Review used exact-revision GitHub reads; local tests were not run because no trusted PR checkout was available.
sent by review-bot (Codex · gpt-6-astra) · open in session
| completedAt: operation.completedAt?.toISOString() ?? null, | ||
| ...(result !== undefined ? { result } : {}) | ||
| ...(result !== undefined ? { result } : {}), | ||
| ...(nativeUi ? { nativeUi } : {}) |
There was a problem hiding this comment.
[P2] Deliver the compact intent independently of the operation text
Hoisting nativeUi fixes the nesting, but getOperation has no UI metadata, so mcp/routes.ts returns its entire DTO as one text block without structuredContent. nativeUiFromToolUpdate skips text blocks longer than 4,096 characters before parsing them. For example, an allowed createAgent request with a 5,000-character description creates successfully after approval, but its getOperation result exceeds this limit and still produces no editor card. Publish the validated compact intent as structured content or a separate bounded text block while preserving the operation’s full answer.
| useEffect(() => { | ||
| if (!sameOrg || loading || agent || asked.current) return | ||
| asked.current = true | ||
| void Promise.resolve(refresh()).finally(() => setRevalidated(true)) |
There was a problem hiding this comment.
[P3] Await the actual roster refresh before marking it complete
useConsoleData().refresh() returns void and starts revalidateConsole() without returning its promise. Consequently, Promise.resolve(refresh()) settles immediately and sets revalidated while the roster request is still in flight, showing “This agent is unavailable” until the fresh row arrives. The new refresh removes the old polling delay, so this is non-blocking, but the loading state should follow an awaitable roster read rather than the fire-and-forget wrapper.
There was a problem hiding this comment.
Approved at 93cab2315b89fdf376d05f7116f441f592350d85. Completed approval results now expose nativeUi through structuredContent, which the daemon reads before applying the text-size limit. Pending operations remain card-free. No remaining blocking findings.
Two non-blocking caveats remain: refresh() still does not return the promise the dialog tries to await, and adapters exposing only text still encounter the 4,096-character fallback limit. A separate compact intent text block would cover those adapters.
Reviewed the exact revisions through GitHub read-only inspection. Local tests were not run because no trusted PR checkout was available.
sent by review-bot (Codex · gpt-6-astra) · open in session
Admin MCP could only open two native surfaces: the integration dialog and the code-host
connections page. This adds the other three configuration surfaces a webchat admin
conversation actually needs, through the same
ui://path — no iframe, no HTML template,no MCP credential in the browser.
What opens now
createAgentconfigureAgentinstallSkillinstallMcpServerEach one is read-only: it opens a form and saves nothing. The human submits it under their
own Console JWT, and the existing REST authorization is the only gate. No secret env var,
MCP header value or OAuth client secret can ride in a tool argument — every intent schema
is strict and rejects them, which is the point of handing this work to the browser.
Notable pieces
protocol— three intents joined toNativeMcpUi, plus one sharednativeUiTitle()so the daemon's card chrome and the Console card cannot word the same surface differently.
createAgentkeeps its own answer. Every other UI tool's whole result is the intent;a write tool cannot afford that. Its body is now the created agent with the intent beside
it under
nativeUi(NativeUiEnvelope), republished as structured content, and the daemonaccepts an intent in either position. A 202 approval-pending creation is passed through
untouched.
agentIdalso enables thenew source or attaches the new server on that agent. That write can fail on its own, so the
summary says which of the two landed rather than claiming both.
canEditbefore mounting anything.
Verification
pnpm typecheckclean.mcp.route.test.tsintegration (35, real Postgres) — all green.
envelope parsed from both structured content and a text block, card titles per resource,
and the dialogs' mount / attach / failure-reporting paths.
🤖 Generated with Claude Code