When using the MCP Apps SDK (@modelcontextprotocol/ext-apps), the ontoolresult notification is only fired once when the tool initially completes. If the user refreshes the page, the iframe reloads and reconnects to the host, but the host never re-sends the last tool-result notification. This leaves the widget stuck on a loading state with no way to recover its previous view.
Steps to Reproduce
Create an MCP App that uses ontoolresult to receive structured content and render UI
(e.g., a flashcard study widget)
Trigger a tool call (e.g., create-deck) the widget receives ontoolresult and
renders correctly
Refresh the page (Cmd+R / F5)
The iframe reconnects to the host, but ontoolresult never fires again
The widget is stuck showing a loading spinner indefinitely
Expected Behavior
After a page refresh, the host should re-send the most recent tool-result notification to
the reconnected app so it can restore its UI state.
This is similar to how onhostcontextchanged provides the current host context on
reconnection, ontoolresult should replay the last result so apps don’t need to implement
their own persistence layer just to survive a refresh.
Current Workaround
Persist the last toolOutput and viewUUID to localStorage manually, and restore from
it on mount. This works but shouldn’t be necessary.
Thanks for raising this. I reviewed the behavior and this looks like expected SDK/host behavior, not a platform bug.
ontoolresult is emitted as part of the tool execution flow. After a page refresh/iframe reconnect, the host does not guarantee replay of the last prior tool-result event. Instead, widgets are expected to rehydrate from persisted state/tool output.
Official docs that align with this:
State management / rehydration expectations: Managing State
So if the widget is stuck loading after refresh, that usually means the app is depending only on a one-shot ontoolresult instead of restore paths.
What you can do to resolve it:
On widget init, read from window.openai.toolOutput (or equivalent hydrated output) before waiting for new events.
Persist minimal UI state with setWidgetState and restore from widget state on mount.
Keep a backend source of truth for critical data (deck/session/result IDs) and re-fetch on reconnect.
Make ontoolresult additive/idempotent: treat it as an update, not initial state bootstrap.
Add a refresh-safe fallback flow: If no fresh event within N ms, recover from persisted state/backend. Show “restoring…” and retry controls instead of infinite spinner.
Add a regression test for tool -> refresh -> iframe reconnect -> restored UI path.