Skip to content

Keep MCP-Apps rendering across a cold session restore - #1504

Merged
RhysSullivan merged 2 commits into
mainfrom
fix-mcp-apps-cold-restore
Jul 30, 2026
Merged

Keep MCP-Apps rendering across a cold session restore#1504
RhysSullivan merged 2 commits into
mainfrom
fix-mcp-apps-cold-restore

Conversation

@RhysSullivan

@RhysSullivan RhysSullivan commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

MCP-Apps rendering was lost on cold session restore: the same session rendered an inline widget before a Durable Object restore and a fallback_url deep link after, and once an isolate lost the capability it served fallbacks for the rest of its life.

What actually happens

The first read of this was that a restored DO has no initialize to replay, so appsEnabled stays false. That is not the mechanism. The agents runtime persists the initialize request and replays it on restore (reinitializeServer), which does re-establish the server's client capabilities.

The gap is that capabilities and the hook are set by two different messages:

  • _clientCapabilities is assigned by the initialize request handler.
  • oninitialized — the only hook that re-runs syncToolAvailability — fires on the notifications/initialized notification, which is never persisted or replayed.

So a restored session holds full apps capabilities while the in-memory flag still reads its seeded value, and nothing ever reconciles the two. The replay is also dispatched un-awaited, so a tool call can land before it arrives. Both orderings produced the deep link.

The fix

deliverArtifact resolves capability from the live server on every render rather than trusting the cached flag, and reconciles the app-only tools when the two disagree — otherwise a restored session could render inline while execute-action, the tool the widget calls back into, stayed hidden. The persisted seed remains the fallback for the window before any capabilities exist.

The persist path is guarded so a reading taken with no capabilities present is never written back; without that, one such replay would make the downgrade durable for every future restore of the session.

The host side (first commit) still persists the negotiated bit alongside sessionMeta and rehydrates it. resolveAndStoreSessionMeta carries the stored value forward because init() re-resolves meta from the bearer token on every restore, which carries no capabilities and would otherwise erase it.

Tests

The headline test replays the initialize request under the SDK's restore sentinel id and sends no initialized notification — the exact shape of a real restore — with restoredAppsEnabled: false, so a pass cannot come from the seed. It asserts the inline payload and that execute-action is listed. Verified by reverting the fix and watching it fail.

The negotiated MCP-Apps capability lived only in the tool server instance,
set from the client at initialize. A session whose Durable Object was
evicted (deploy, idle) and cold-restored on the same mcp-session-id was
rebuilt with no initialize to replay, so apps support read back as false
and create-artifact/show-artifact answered with the deep-link fallback
instead of the inline widget, mid-conversation.

Persist the negotiated bit in SessionMeta and seed the rebuilt server
from it. A real initialize still overrules the restored value in both
directions, and init carries the stored bit through its re-resolve so
restoring a session no longer erases it.
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 30, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 08cc11b Commit Preview URL

Branch Preview URL
Jul 30 2026, 05:39 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 30, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
executor-cloud 08cc11b Jul 30 2026, 05:38 AM

@pkg-pr-new

pkg-pr-new Bot commented Jul 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1504

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1504

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1504

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1504

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1504

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1504

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1504

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1504

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1504

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1504

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1504

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1504

executor

npm i https://pkg.pr.new/executor@1504

commit: 08cc11b

The persisted-capability seed alone did not fix the production symptom.
On cold restore the agents runtime replays the persisted initialize
REQUEST, which does re-establish the server's client capabilities — but
it never replays the notifications/initialized NOTIFICATION, and
oninitialized (the only hook that re-runs syncToolAvailability) fires
solely on that notification. A restored session therefore held full apps
capabilities while the cached flag still read its seeded value, and the
replay is dispatched un-awaited, so a tool call can also land ahead of
it.

deliverArtifact now reads the live server's capabilities per render and
reconciles the app-only tools when they disagree, so both orderings
answer identically. The seeded value remains the fallback for the window
before any capabilities exist.

Also guard the persist path: a reading taken with no capabilities present
must never be written back, or one such replay would make the deep-link
downgrade durable for every future restore of the session.
@RhysSullivan
RhysSullivan marked this pull request as ready for review July 30, 2026 16:41
@RhysSullivan
RhysSullivan merged commit 0e8b65a into main Jul 30, 2026
20 of 21 checks passed
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