Skip to content

Show MCP approval reviews inside the app, in its own theme - #8084

Merged
3mdistal merged 56 commits into
mainfrom
mcp/approval-review-page
Oct 27, 2026
Merged

3mdistal merged 56 commits into
mainfrom
mcp/approval-review-page

Conversation

@3mdistal

@3mdistal 3mdistal commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds the themed app review surface on top of #8016's approval flow, which has landed on main.

What changes

When an MCP client cannot show a person's approval prompt, #8016 returns a link to a plain review page. This PR opens the same review inside an opted-in app, using its name, logo, theme, and components. The person can inspect the call, approve or deny it, and return to the app.

  • Signed-in readers are redirected to the app's home route with #mcpApproval=<id>. The fragment survives home-route redirects, and AppProviders shows the review in place of the app. Sign-in continues at the original review link.
  • appApprovalPage is off by default. Apps opt in through the chat plugin or mountMCP only when their shell renders AppProviders. The 16 chat templates, Dispatch, and Builder Code starter opt in. Plan opts in on its separate MCP mount. The default scaffold remains off.
  • The card identifies the requesting client and connection date, recipients, stored fields, times in the reader's zone, and any prior-run or shortened-storage warning. After a decision it retains a short description of the request so several open reviews remain distinguishable.
  • The shared summary removes repeated recipient and time-zone rows while retaining separate to, cc, and bcc fields. Schema choice titles, including titled booleans, supply readable values. Identifier-style enums are split into words, and time zones use localized names. The plain page and client form share these improvements.
  • Email addresses remain copyable unchanged and wrap only after @. Recipient text shrinks when needed on narrow screens. Approve uses the theme's foreground and background colors.
  • Lost or unreadable decision responses trigger a state reread. At the deadline the server determines whether the review expired or the grant was used. Timing uses the server's servedAt value, so a fast or slow device clock does not change the decision window.
  • The review appears only in the top-level private app, outside onboarding and the phone handoff. It stays out of frames, public pages, and session-bypassed surfaces. Custom authentication without the cookie-only browser hook gets a distinct sign-in-unavailable state.

The browser reads and decides through the new @agent-native/core/client/mcp-approval helpers. Decisions retain #8016's session, ownership, token, and same-origin checks. The app surface requires the standard framework route prefix; other MCP prefixes keep the plain page. New copy is translated into all 11 locales, and affected apps receive changelog entries.

Verification

  • Restacked onto main after Let people approve MCP calls on a review page when their client can't ask #8016 squash-landed as 8254996c: merged main up to the commit before the squash, recorded the squash with -s ours since this branch already had its changes, and checked that the diff against main covers the same 56 files as before. The Toolkit review and AppProviders specs (62 tests) and the Core approval route, summary and review-helper specs (81 tests) pass on 04333606.
  • The review gate reads the window while rendering, so it must never render on the server. AppProviders keeps it inside ClientOnly, and a new test server-renders AppProviders with window stubbed out. If the gate moves in front of ClientOnly, the test fails with Cannot read properties of undefined (reading 'top').
  • Merged Let people approve MCP calls on a review page when their client can't ask #8016's head 97a75888, which now includes main through Ask a person before CLI calls run actions that need approval #8117, without conflicts. The earlier AppProviders.tsx conflict kept both main's lazy agent-run awareness component and this PR's review boundary, and main's strict lint flag on a dead ?? "" was fixed by finding the time placeholder by position.
  • Main's strict compiler check flagged the review page passing an undefined abort signal where fetch takes null; it now passes null. On the merged tree Core typechecks, and the oracle-loosening, strict-lint and escape-hatch guards pass. The release test that pins Toolkit's Core floor passes.
  • A lone cc or bcc now keeps its own row on the review page, so a copy doesn't read as sent to; a lone to or attendees list of addresses is still left to Reaches. The summary, route, Toolkit review and Calendar MCP write specs pass (77, 30 and 33 tests).
  • The grant id and fragment helpers in shared/mcp-approval-review.ts now have their own spec beside them, which the changed-line mutation job needs to test that file.
  • The review view now takes its servedAt time and state after its async reads (requester, prior run, decision token), so read latency no longer looks like clock skew to the page; a grant that expires during those reads comes back expired. A new route test covers it and fails on the previous code.
  • On the previous merged tree (674b15fd), 385 of 386 Core approval, redaction and mount tests and 61 Toolkit review and AppProviders tests pass. The other Core test, the ChatGPT scan replay in server.spec.ts, hit its 60 s timeout with this machine's CPU at 99% from other work; it passed alone in 30 s on the previous head. strict-lint passes, and escape hatches show no growth across the 59 changed files.
  • Restacked only this PR's ten commits onto the new Let people approve MCP calls on a review page when their client can't ask #8016 head. The grant-ID check retains both the routed-path helper and this PR's shared UUID validator.
  • 430 focused tests pass across 13 suites: the route redirect, base/home paths, browser helpers, summaries, mounted routes, plugin options, template opt-ins, review rendering, AppProviders, Dispatch, and CRM catalog options.
  • The inherited real-H3 mount regression checks differing raw and routed paths for review reads and decisions, with persisted grant state.
  • Format check, oxlint, direct core typecheck, and Core and Toolkit builds pass. The direct typecheck avoids the known Windows workspace-launcher spawn EINVAL failure in aggregate pnpm lint.
  • All 93 guards pass with Git's GNU utilities on PATH, including both localization guards and the Builder Code template-layer check.
  • Earlier local Calendar browser acceptance is historical evidence: light and dark themes, desktop and 390 px screens, approval and retry, prior-run warnings, and seven recipients. This rebase reruns focused automated checks; it does not claim a new browser run.

Before merge

Toolkit's optional Core peer is now >=0.221.0. Both changesets bump Core as minor, so 0.221.0 is the first release that will carry client/mcp-approval; no published Core exports it yet. Main's Core is still 0.220.0. If a Core release goes out before this PR lands, the floor moves to the release after it.

Remaining scope

Calendar's sendUpdates schema can separately add option titles such as “All guests” to improve the generic labels.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Visual recap — generation failed

The visual recap could not be generated for this pull request. This is informational only and does not block the PR.

Diagnostic:

No plan URL: Repair changed too much of targeted file plan.mdx; expected a localized parser fix.

Agent output: Repaired recap-source.json: escaped the Mermaid and diff code strings that contained parser-breaking literal newlines. JSON and required block IDs remain valid. --- ⠀ 🟢 Visual recap source repaired and ready for the hosted parser retry. stderr: Reading additional input from stdin...

builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

@3mdistal
3mdistal force-pushed the mcp/approval-review-page branch from bff38d5 to 619f58e Compare October 23, 2026 01:49
builder-io-integration[bot]

This comment was marked as outdated.

@3mdistal
3mdistal force-pushed the mcp/approval-review-page branch from 619f58e to 07645e1 Compare October 23, 2026 02:22
builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

@3mdistal
3mdistal force-pushed the mcp/approval-review-page branch from c3d55ab to b5dda3d Compare October 23, 2026 03:35
builder-io-integration[bot]

This comment was marked as outdated.

@3mdistal
3mdistal force-pushed the mcp/approval-review-page branch from b5dda3d to 5a97854 Compare October 23, 2026 04:23
builder-io-integration[bot]

This comment was marked as outdated.

@3mdistal
3mdistal force-pushed the mcp/approval-review-page branch from 5a97854 to b412c80 Compare October 23, 2026 04:36
builder-io-integration[bot]

This comment was marked as outdated.

@3mdistal
3mdistal force-pushed the mcp/approval-review-page branch from b412c80 to d537563 Compare October 23, 2026 04:49
builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

@3mdistal

3mdistal commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor Author

On the low finding in the 133efc4 review (a 401 on Approve or Deny leaves no in-page way back): leaving this as is. The review link the agent gave is where sign-in happens. Opening that link again signs the reader in and brings them back to the same review. That works because the decision was never saved and the grant is still pending until its deadline. A session that ends inside the ten-minute review window, such as a sign-out in another tab, is rare. Adding a sign-in control to the review would be a new visual element, and this PR is closed to further visual changes. If it's wanted, it can be a follow-up shared with #8016's plain page, which says the same thing.

… ask

A needsApproval action called over MCP now returns a normal tool result
with a review-page link when the client can't show an approval prompt,
including every stateless 2025-era client that used to get -32021, or
when it declines or cancels the prompt. The page needs the same signed-in
user and org, renders only the stored redacted arguments, and takes
Approve or Deny as a same-origin JSON POST with a per-user token. A page
approval lets the identical call run once within ten minutes; Deny is
final.

The page and the in-client form share one server-side summary: times in
the reader's zone with labels, the requested zone when it differs,
zoneless times labelled (Calendar's create-event declares
zonelessTimes: "calendar"), the authorized OAuth connection, and a
warning when the same call already ran unless the action is retrySafe.
Page approvals are audited as mcp-review.

The grant store gains additive columns, joins the sensitive framework
tables, and is registered for identity rekey and offboarding.
…and clear stored arguments

The review page now finds the reviewer only from the browser's sign-in
cookie (Better Auth's or the framework's own), never from getSession, so
bearer, embed, query and custom-resolver credentials can't decide a grant.
Grants carry the app's canonical id in the caller key and every read,
decision and consume matches it, so apps sharing a database can't use
each other's approvals. Stored arguments are cleared when a grant is
used, denied or declined, hidden once it expires, and pruned by a
recurring sweep. The page's JSON view and POST require
Sec-Fetch-Site: same-origin as CSRF protection, and a reader's own grant
from another organization says to switch organizations.
… app's own origin

The review page now lists every recipient and argument whole and says
plainly when storage had to cut the stored arguments. The in-client form
shows at most 20 recipients, 12 fields and 280 characters a value, says
what it left out, and links the page when the page shows more.

Approval links use the request's origin only when the app declares it
(or declares none), as sign-in redirects do. Calendar's zonelessTimes
survives both action loaders. Impossible offsets and dates are shown as
written instead of converted. A page approval still runs the call when
the prompt that asked for it has expired; that prompt's own answers no
longer count.
…e text, and read an end in its start's zone

Sweep handlers only run from the platform sweep route, which nothing calls
on a long-lived server, so the approval route also prunes on its own
ten-minute timer there. The shared argument redactor now removes a
credential inside ordinary text (Authorization: Bearer ..., password=...),
as its text summaries already did. The approval summary reads an end time
with no zone of its own in its start's zone, as Calendar does.
…n, and leave a skipped DST time as written

- mcpApprovalReviewUrl reads the approval route from the request's MCP mount, so mountMCP with a custom routePrefix links to the page it mounted.
- declaredLinkOrigin keeps the request's origin when the app declares none; Netlify's URL and DEPLOY_URL don't count, as for Builder connect.
- A wall time its zone skips (spring-forward gap) reads as no instant and is shown as written, not as a time with no zone.
- A signed-out reader in an app with no sign-in page gets a short page in their language instead of an English JSON error; JSON callers get the same text.
- A stale decision token answers in the reader's language, and the page says to reload instead of to try again.
- mountMcpApprovalRoute registers <route>/:id with registerAuthPublicPaths, so a custom routePrefix under /_agent-native reaches the page's own sign-in instead of the guard's bare 401.
- Exact registered public paths accept a :name segment for one path segment, and the guard no longer hard-codes the default review path.
builder-io-integration[bot]

This comment was marked as outdated.

…lback

# Conflicts:
#	packages/core/src/audit/types.ts
#	packages/toolkit/src/app/i18n/catalogs/ar-SA.ts
#	packages/toolkit/src/app/i18n/catalogs/de-DE.ts
#	packages/toolkit/src/app/i18n/catalogs/en-US.ts
#	packages/toolkit/src/app/i18n/catalogs/es-ES.ts
#	packages/toolkit/src/app/i18n/catalogs/fr-FR.ts
#	packages/toolkit/src/app/i18n/catalogs/hi-IN.ts
#	packages/toolkit/src/app/i18n/catalogs/ja-JP.ts
#	packages/toolkit/src/app/i18n/catalogs/ko-KR.ts
#	packages/toolkit/src/app/i18n/catalogs/pt-BR.ts
#	packages/toolkit/src/app/i18n/catalogs/zh-CN.ts
#	packages/toolkit/src/app/i18n/catalogs/zh-TW.ts
#	packages/toolkit/src/app/settings/shell/pages/audit.tsx
builder-io-integration[bot]

This comment was marked as outdated.

@3mdistal

3mdistal commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

On the 🟡 LOW from the 382399b6 review (Dispatch's Core peer range): I'm keeping Dispatch's range as it is.

  • Dispatch's own code still runs on Core 0.220.x. mcp.appApprovalPage is a server option, and 0.220.x's agent-chat plugin ignores mcp keys it doesn't know. That Core has no review-link fallback either, so the option would have nothing to route.
  • The part that needs 0.221.0 is Toolkit's AppProviders, which imports @agent-native/core/client/mcp-approval. Toolkit's optional Core peer is now >=0.221.0, and Dispatch depends on Toolkit through workspace:*, which publishes as the same fixed-group version. An install that pairs this Dispatch with Core 0.220.x therefore already gets Toolkit's unmet-peer warning.
  • Dispatch's >=0.220.1-0 || >=0.221.0-0 comes from Fix embedded Workspace app session handoff #8227, and package-release-workflow.test.ts pins it exactly so Dispatch accepts Core prerelease snapshots on both the 0.220.1 and 0.221.0 version lines. Raising it here would undo that for a requirement Toolkit already declares.

builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

Base automatically changed from mcp/approval-link-fallback to main October 27, 2026 07:31

@builder-io-integration builder-io-integration 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.

Builder reviewed your changes and has a few items to flag 🟡

Review Details

Incremental Review Summary

This update keeps the core MCP approval flow and themed in-app review surface intact, with the latest addition protecting the review boundary from server rendering through ClientOnly. I verified that the previous SSR browser-global issue is fixed: ProvidersInner (and thus McpApprovalReviewBoundary) is now rendered only after the client mounts, and a server-render test asserts the review stays out of markup when window is absent. Its prior thread has been resolved. The approval and grant-state paths otherwise appear sound in the current review.

Finding

  • 🟡 Low — Schema labels can hide the actual argument value: describeFieldValue substitutes a schema-authored title/description for the literal value in the primary summary. A reviewer could see an inaccurate label while approving the actual argument; show the literal alongside the label or keep the label secondary. This may be filtered by review policy.

🧪 Browser testing: Skipped — dev server setup fails during Toolkit postinstall with 93 TS2883 React type errors; restart reproduced the failure.

@3mdistal

Copy link
Copy Markdown
Contributor Author

On the 🟡 about schema labels hiding the literal value: the literal is always on the page. Both the in-app review and the plain page show the call's stored arguments, unchanged, under Details. A label replaces a value only when that value exactly matches a const (or a titled enum word) in the action's own input schema. The app that runs the action writes that schema, so neither the agent nor the MCP client can choose the label. A value with no matching option is shown as written. I'm leaving it as it is: the labels are there so a person can read a choice like externalOnly without knowing the action's code.

@3mdistal
3mdistal merged commit 12a91c6 into main Oct 27, 2026
103 of 105 checks passed
@3mdistal
3mdistal deleted the mcp/approval-review-page branch October 27, 2026 08:04
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.

3 participants