Skip to content

feat(mcp): open the agent editor, skill installer and MCP installer as native apps - #2129

Merged
spacedragon merged 3 commits into
mainfrom
dev/yulong/kahawai
Sep 17, 2026
Merged

spacedragon merged 3 commits into
mainfrom
dev/yulong/kahawai

Conversation

@spacedragon

Copy link
Copy Markdown
Contributor

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

Tool Surface
createAgent the new agent's own editor, as the creation's card
configureAgent the Console agent editor on an existing agent, optionally on a section
installSkill the skills.sh registry search (optionally preseeded) or the Git import
installMcpServer "Add MCP server"

Each 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 to NativeMcpUi, plus one shared nativeUiTitle()
    so the daemon's card chrome and the Console card cannot word the same surface differently.
  • createAgent keeps 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 daemon
    accepts an intent in either position. A 202 approval-pending creation is passed through
    untouched.
  • Second writes are reported separately. An installer given an agentId also enables the
    new 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.
  • Each dialog re-checks the active organization, the viewer role and the agent's canEdit
    before mounting anything.

Verification

  • pnpm typecheck clean.
  • control-plane unit (2536) · web (1861) · protocol · daemon MCP suites · mcp.route.test.ts
    integration (35, real Postgres) — all green.
  • New coverage: the three tools' intents and their refusal of credential/org arguments, the
    envelope parsed from both structured content and a text block, card titles per resource,
    and the dialogs' mount / attach / failure-reporting paths.
  • eslint + prettier clean.

🤖 Generated with Claude Code

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

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

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

Comment on lines +17 to +21
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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

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.

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

Comment on lines +31 to +33
if (loading) return notice('Loading configuration…')
const agent = agents.find((item) => item.id === ui.intent.agentId)
if (!agent) return notice('This agent is unavailable.')

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

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

spacedragon and others added 2 commits September 17, 2026 02:53
…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>

@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 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 } : {})

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] 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))

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.

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

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

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

@spacedragon
spacedragon merged commit 0bb2e0a into main Sep 17, 2026
10 checks passed
@spacedragon
spacedragon deleted the dev/yulong/kahawai branch September 17, 2026 03:09
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