Repository navigation
Conversation
Co-authored-by: ochafik <ochafik@anthropic.com>
Co-authored-by: ochafik <ochafik@anthropic.com>
Co-authored-by: ochafik <ochafik@anthropic.com>
Co-authored-by: ochafik <ochafik@anthropic.com>
Co-authored-by: ochafik <ochafik@anthropic.com>
…eact servers Co-authored-by: ochafik <ochafik@anthropic.com>
…dget state Co-authored-by: ochafik <ochafik@anthropic.com>
Contributor
📖 Docs Preview Deployed
Includes drafts and future-dated posts. All pages served with |
@modelcontextprotocol/ext-apps
@modelcontextprotocol/server-basic-preact
@modelcontextprotocol/server-basic-react
@modelcontextprotocol/server-basic-solid
@modelcontextprotocol/server-basic-svelte
@modelcontextprotocol/server-basic-vanillajs
@modelcontextprotocol/server-basic-vue
@modelcontextprotocol/server-budget-allocator
@modelcontextprotocol/server-cohort-heatmap
@modelcontextprotocol/server-customer-segmentation
@modelcontextprotocol/server-debug
@modelcontextprotocol/server-lazy-auth
@modelcontextprotocol/server-map
@modelcontextprotocol/server-pdf
@modelcontextprotocol/server-scenario-modeler
@modelcontextprotocol/server-shadertoy
@modelcontextprotocol/server-sheet-music
@modelcontextprotocol/server-system-monitor
@modelcontextprotocol/server-threejs
@modelcontextprotocol/server-transcript
@modelcontextprotocol/server-video-resource
@modelcontextprotocol/server-wiki-explorer
commit: |
…aved view state Co-authored-by: ochafik <ochafik@anthropic.com>
Contributor
Author
|
Generated by Claude Code |
Co-authored-by: ochafik <ochafik@anthropic.com>
Co-authored-by: ochafik <ochafik@anthropic.com>
Contributor
Author
|
On the "unused function" and "unused variable" findings from the code-quality bot: I am leaving these as they are. They are all in the type-checked example files ( Generated by Claude Code |
ochafik
marked this pull request as ready for review
November 4, 2026 16:31
This branch has not been deployed
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.
Addresses #417 and #465.
Note
Draft proposal, opened for discussion. The open questions at the end are real ones.
Problem
A View loses its in-memory state whenever its iframe is destroyed: on page reload, when a conversation is reopened, or when the Host unmounts it. The Host re-sends the tool input and result, but not what the user did afterwards.
The documented pattern,
localStoragekeyed by a server-madeviewUUID, depends on how each Host assigns sandbox origins (#213), cannot follow the user to another device, and is not deleted with the conversation.It is also the main gap left for apps that come from the OpenAI Apps SDK (#465).
Prior art and credit
The idea is OpenAI's. In the OpenAI Apps SDK,
window.openai.widgetState/setWidgetState()give a widget persistence that is scoped to one rendered instance, and its structured shape separates what the model sees from what only the UI sees. Credit to its authors: this PR brings the UI-only half to MCP Apps, under names that fit this spec.window.openai.widgetStatehostContext.viewState(this PR)setWidgetState(state), or theprivateContentof its structured shape ("UI-only state the model should not see")ui/set-view-state(this PR)modelContent("text or JSON the model should see")ui/update-model-context(exists)imageIdsTwo more pieces of context:
hostContext["openai/modelContext"]on initialization or remount, which restores the model-visible half and is complementary to this PR.In this repository, #194 first proposed
setWidgetStateand was closed in favour ofui/update-model-context(#125), which covers what the model sees. This PR covers the other half: what only the View sees.Proposal
All of it is optional for Hosts.
ui/set-view-state { state }→{}. Replaces the saved state as a whole;nullclears it. Refusals:-32602(never as given, for example larger thanmaxBytes),-32603(not now). No key parameter: the Host derives the key from the user, the server connection and the View instance.hostContext.viewState. A Host that advertises the capability includes the saved state, ornull, in everyui/initializeresult, and never sends it inui/notifications/host-context-changed: saves are not echoed.hostCapabilities.viewState: { maxBytes }. A Host that has not read the saved state when it answersui/initializedoes not advertise it for that render, so a View cannot overwrite state it has not seen.Scope: one View instance, the View that the Host renders for one tool call (whoever initiated the call), however often it renders it. A wider scope (per conversation, per device, per account, as raised in #417) would be a separate capability with its own rules.
Schema change
Example
What is in this PR
spec.types.tsand the regenerated schemas.App.setViewState(state). It sends nothing unless the Host advertised the capability. On success it setsgetHostContext().viewStateto what it sent, without callinghostcontextchangedlisteners.AppBridge.onsetviewstate. The bridge calls the handler for one save at a time in the order received, coalesces the saves that arrive while one is in progress (only the newest is stored, and they all get its outcome), keeps the last saved state for a laterui/initializeon the same bridge, and never sendsviewStateinhost-context-changed.useViewState(app, initial), likeuseState, restored from the Host and saved back with a debounce.examples/basic-hostnow keeps its tool calls and each call's view state in its ownlocalStorage, so both survive a reload of the page.map-server(camera position) andpdf-server(current page) save throughsetViewStateand fall back toviewUUID+localStorageon Hosts without the capability.basic-server-reactkeeps its unsent drafts withuseViewState.setViewStateand contrasts it with theviewUUID+localStoragefallback. The OpenAI migration guide and themigrate-oai-appskill carry the mapping above.Compatibility
Additive. A Host without the feature sends neither
hostCapabilities.viewStatenorhostContext.viewState. A View without it ignores both: the SDK keeps unknown host-context keys and drops unknown capability keys. Code that validates theui/initializeresult strictly against an olderschema.jsonrejects a result that carrieshostCapabilities.viewState, like any new capability.Security considerations
null, so saved state gives a View no identifier that carries from one conversation to another.Alternatives considered
ui/update-model-context: meant for the model, and never handed back to the View.ui/get-view-staterequest: a second round trip before first paint, for what every View needs at start.setWidgetState({ modelContent, privateContent })does:ui/update-model-contextalready exists with its own content-block shape and modality negotiation, and keeping the private half separate lets the spec say plainly that it never reaches the model.Open questions
ui/initialize. Is that acceptable to Hosts with remote stores, or is a later delivery needed? As drafted, a View cannot tell a Host whose store is slow or down from one with no store (raised in View state persistence #417).null.)stateis an object ornull. Is any JSON value needed?maxBytes?-32602/-32603split wanted, where other requests show-32000?null?Testing
App.setViewStateandAppBridge.onsetviewstate(ordering, refusals and their error codes, the capability check, a secondui/initialize).tests/e2e/view-state.spec.ts(state restored after a reload of the host page with no secondtools/call, a new call starts withnull, an oversize save is refused and the last state kept, closing a call deletes its state) andtests/e2e/pdf-page-restore.spec.ts(the pdf viewer on both the view-state path and thelocalStoragefallback).useViewState's save path has no unit tests (the repo has no DOM test environment; e2e covers the debounced save and restore), andmap-server's port was only exercised against a stubbed Cesium.Related
#213, #62, #427
AI use
Drafted with AI assistance (Claude). Opened as a Draft: I have not finished reviewing every line yet, and will before marking it ready.
Generated by Claude Code