fix(mcpapps): prompt browser performs the platform session handshake#1035
Merged
Conversation
…1032) The prompt browser calls platform_info before its first data call and threads the returned session_id on every subsequent call, so it renders in a deployment with the session handle gate on instead of showing a SESSION_REQUIRED banner. An app's tool calls reach the server over the same MCP transport as an agent's and meet the same gate; platform_info is app-callable because MCP Apps tool visibility defaults to ["model", "app"] and scopes "app" per server connection rather than per app-tool binding. callTool returns {promise, cancel} and owns the whole session concern: it waits on an in-flight handshake rather than issuing with a handle it just discarded, merges the handle into the arguments in one place, and recovers once from SESSION_REQUIRED, SESSION_EXPIRED, or SETUP_REQUIRED by re-handshaking and replaying. cancel() drops the request and blocks a replay that has not gone out, so a superseded search cannot leave a second waiter behind. A deployment with handles disabled returns no session_id and nothing is threaded. A failed handshake is not fatal. The list call runs regardless and reports whatever the platform says, because handles may be off and a persona may allow manage_prompt while denying platform_info. Response correlation on hosts that deliver results as notifications carries two hazards, both now handled. The platform_info payload includes a prompts array listing the server's registered MCP prompts, so a list result is identified by count plus the absence of config_version and features rather than by a prompts array alone. A session refusal invalidates the handle for every in-flight gated call, so it settles all of them and each recovers through the shared handshake; the handshake's own waiter is excluded, since platform_info is never gated and so is never the call being refused. The test harness enforces the gate rather than describing it: it mints a fresh handle on platform_info, refuses any other call whose handle is missing or stale with the platform's own refusal text, and carries the prompts array so the correlation collision reproduces in development. An Expire Session control revokes the live handle to exercise the recovery path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1032
The prompt browser MCP App added in #1011 could not make a single tool call in a deployment with the session handle gate on. Its first action after
ui/initializeis amanage_promptlist, which the platform refuses withSESSION_REQUIREDbecause the call carries noplatform_info-mintedsession_id, so the app rendered its own error banner instead of the library. List, search-as-you-type, and Run were all dead.An app's tool calls reach the server over the same MCP transport as an agent's and meet the same gates.
pkg/middleware/mcp_session_handle.go:173exempts only the init tool, operator-configured exempt tools, and the stateless in-memory shims (Source=rest/Source=admin); a host-proxied app call arrives asSource=mcpand is gated exactly like an agent call. So the app now performs the platform handshake itself, the same contract every other caller follows. No server-side gate changes.The handshake
platform_infois app-callable. MCP Apps tool visibility defaults to["model", "app"], and the spec scopes"app"per server connection rather than per app-tool binding (ext-apps/specification/2026-01-26/apps.mdx:399-401), so an app bound tomanage_promptmay still call it. Its result's first text block is the JSON payload carryingsession_id(pkg/platform/info_tool.go:283-320).callToolowns the whole session concern and returns{promise, cancel}:session_idinto the arguments in one place, covering list, search, and use.SESSION_REQUIRED,SESSION_EXPIRED, orSETUP_REQUIREDby discarding the handle, re-handshaking, and replaying.SETUP_REQUIREDis the transport-keyed session gate (pkg/middleware/mcp_session_gate.go:212) a deployment may run instead of explicit handles; its remedy is the same call.cancel()drops the request and blocks a replay that has not gone out yet, so a superseded search cannot leave a second waiter behind.A deployment with handles disabled returns no
session_id, in which case nothing is threaded and behavior is unchanged.A failed handshake is not fatal. The list call runs regardless and reports whatever the platform actually says. Handles may be off, and a persona may allow
manage_promptwhile denyingplatform_info(pkg/persona/filter.gois default-deny with no exemption for it), so blanking the app on aplatform_infofailure would break cases that worked before the handshake existed. With the gate on and the handshake refused, the app self-heals through the list call's own recovery and never shows a banner.Response correlation
Hosts speaking protocol
2025-01-09deliver results asui/notifications/tool-resultnotifications, which carry no request id, so the app matches responses by payload shape. Adding a second kind of call introduced two hazards, both handled here.The
platform_infopayload carries apromptsarray listing the server's registered MCP prompts (Info.Prompts,pkg/platform/info_tool.go:31), confirmed live at 5 entries against a running server. Identifying a list result by apromptsarray alone would therefore let the handshake response satisfy a waiting list call and render the server's MCP prompt list in place of the user's library. A list result is now identified bycountplus the absence ofconfig_versionandfeatures, both of which are unconditional on the info payload and appear on nomanage_promptpayload.A session refusal invalidates the handle for every in-flight gated call, so it settles all of them and each recovers through the shared handshake. Settling only the oldest waiter would make an unrelated call perform the recovery while the refused one hung to its 20 second timeout. The handshake's own waiter is excluded, since
platform_infois never gated and so is never the call being refused.Test harness
apps/test-harness.htmlenforces the gate rather than describing it, so an app that skips the handshake fails in development instead of only in production:platform_infomints a fresh handle per call and supersedes the last, as the platform does.SESSION_REQUIRED/SESSION_EXPIREDtext as anisErrorresult.promptsarray, so the correlation collision above reproduces in the harness rather than being hidden by a simplified fixture.The
platform-infoapp is excluded from the minting branch, sinceplatform_infois the tool it renders and its call must still receive the editable test data.Verification
Driven against a real server (HTTP transport, Postgres, handle gate on by default) with the app in an MCP Apps host bridged to the live MCP endpoint, on both the
2026-01-26and2025-01-09protocol paths:manage_promptwith no handle returnsSESSION_REQUIRED;platform_infomints adps_handle; the same call carrying it succeeds; an unknown handle returnsSESSION_EXPIRED.audit_logsrows for the app'smanage_promptcalls carry the caller identity and the resolveddps_handle assession_id.session_idis injected intomanage_prompt's input schema ontools/listand absent fromplatform_info's.Against a host that enforces the gate under controlled latency and expiry:
usecall refused together: both settle promptly, both replay on one handshake, Run completes.session_idargument sent, library renders.make verifypasses.Documentation
docs/mcpapps/development.mdgains a "Calling Tools from an App" section covering the handshake, the expired-handle recovery path, the rule that a failed handshake must not be fatal, the correlation hazard, and what the harness enforces, plus a troubleshooting row.docs/mcpapps/overview.mdgains a short "Apps Calling Tools" section pointing at it. Mirrored intodocs/llms.txtanddocs/llms-full.txt.