Recover MCP Apps notebook URL when hosts drop _meta#212
Merged
Conversation
Some MCP hosts strip _meta and structuredContent from tool results (ext-apps#696), so the viewer apps never receive the notebookUrl and can only show the text/image fallback. Cloud-notebook results now also carry an opaque <nbid>HEXID</nbid> marker in their (non-dropped) text content, where HEXID is the deployed notebook's base file name. When a viewer gets a result with no notebookUrl, it extracts the id and issues a resources/read for ui://wolfram/notebook-url/<id>; the server reconstructs the cloud URL from the id statelessly (a CloudObject URL is derived locally from the base name and the user's cloud path) and returns it as the resource text, which hosts do not strip. resources/read is used instead of tools/call because the spec lets apps read any resource URI without the resolver becoming a model-visible tool. Server: - makeNotebookUIResult builds the UI result and appends the marker (cloud URLs only; inline notebooks have no reconstructable id) - readUIResource routes ui://wolfram/notebook-url/<hexId> to readNotebookURLResource, validating the id is hex before rebuilding Viewers: strip the marker in stripAgentOnlyText, extract the id, resolve via resources/read, then embed; a result-sequence guard discards a stale resolve. notebook-viewer applies the same recovery as a result-path fallback (it normally gets its URL from the tool input). Adds tests in MCPApps.wlt and documents the workaround in mcp-apps.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Y8TNLJ63zJor51rq99TqQ
The previous commit recovered the URL by having the app read ui://wolfram/notebook-url/<hexId> via resources/read. That does not work on the hosts we care about: a host that drops _meta/structuredContent also does not forward app-initiated resources/read, answering it with JSON-RPC -32601 "Method not found", so the id -> URL round-trip never completes. Switch to a self-contained approach. The one channel that survives is the tool result content, so makeNotebookUIResult now embeds the URL directly in the (non-dropped) text content inside an <internal>...<url>...</url></internal> marker. The wrapper text is addressed to the model (the notebook is already shown; do not use the URL); the viewers extract the URL locally via findNotebookUrl -> extractNotebookUrlMarker with no server round-trip, and strip the whole marker from the display. Removes the now-dead server resolver (readNotebookURLResource, resolveNotebookURLFromID, the hex-id helpers, the ui://wolfram/notebook-url routing) and reverts readUIResource to its registry-only form, along with the client resolveNotebookUrl/latestResultSeq machinery. Net -222 lines. Verified end-to-end against real tool output: the extracted URL matches _meta.notebookUrl, the marker is stripped from the user-facing display, and the real output is preserved. Updates tests and docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Y8TNLJ63zJor51rq99TqQ
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves MCP Apps notebook embedding reliability in hosts that strip _meta and structuredContent from tool results by adding a content-based fallback: cloud notebook URLs are embedded into the tool result content as a self-describing <internal>…<url>…</url></internal> marker that viewers can extract locally and then strip from user-facing text.
Changes:
- Server-side: add
makeNotebookUIResultto includenotebookUrlin_meta/StructuredContentand (for cloud URLs) append a URL marker intocontent. - Viewer-side: add
extractNotebookUrlMarkerfallback to recover the URL from content when_meta/structuredContentare missing, and strip<internal>…</internal>blocks from displayed text. - Tests/docs: add coverage for marker creation/extraction behavior and document the dropped-
_metaworkaround.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/MCPApps.wlt | Adds tests validating marker appending, extractability, and $Failed behavior. |
| Kernel/UIResources.wl | Implements makeNotebookUIResult and marker helpers used by UI-enhanced tool results. |
| Kernel/Tools/WolframLanguageEvaluator.wl | Switches evaluator UI result construction to makeNotebookUIResult. |
| Kernel/Tools/WolframAlpha.wl | Switches WolframAlpha UI result construction to makeNotebookUIResult. |
| Kernel/CommonSymbols.wl | Declares makeNotebookUIResult as a shared symbol in the Common context. |
| docs/mcp-apps.md | Documents the marker-based URL recovery behavior and constraints. |
| Assets/Apps/evaluator-viewer.html | Adds marker extraction fallback + strips <internal>…</internal> from rendered text. |
| Assets/Apps/wolframalpha-viewer.html | Adds marker extraction fallback + strips <internal>…</internal> from rendered text. |
| Assets/Apps/notebook-viewer.html | Adds result-based URL recovery fallback when tool input isn’t delivered. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The tool-result comments described an opaque <nbid> content marker, but the implementation embeds the URL in an <internal>...<url>...</url></internal> marker (see notebookURLMarkerText in UIResources.wl). Align the comments in both tool files with the actual marker format. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UTmWMZ5thpvab5pgqUX4LJ
makeNotebookUIResult populates both _meta and structuredContent for every deployed value, so 'delivered via _meta only' was inaccurate. Note that inline notebooks go through _meta/structuredContent and are never embedded in the content. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UTmWMZ5thpvab5pgqUX4LJ
extractNotebookUrlMarker returned the FIRST <internal>...<url>...</url> </internal> marker in the content. Because the text content is built from tool output (stringResult) that can carry externally-sourced text, a crafted marker injected earlier in the content could override the server-appended URL when a host drops _meta/structuredContent, causing the viewer to load an attacker-chosen notebook. The server always appends its marker as the final content item (appendNotebookURLMarker), so scan for the LAST marker in document order instead. This also holds if a host concatenates the text items, since the trusted marker remains last. Legitimate single-marker results are unchanged. Applied identically to all three viewers (evaluator, notebook, wolframalpha). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014YoEwcu8kbrESPvQFsXyxp
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.
Problem
MCP Apps viewers embed a cloud notebook by reading
notebookUrlfrom the tool result's_meta/structuredContent. Some hosts strip both from tool results (ext-apps#696), so the viewer never receives the URL and can only show the plain text/image fallback instead of the interactive notebook.Solution
For UI-enhanced cloud-notebook results, the tool now also embeds the URL in the one channel the host does not strip — the tool result
content— inside a self-describing marker:The wrapper text is addressed to the model (the notebook is already shown; don't act on the URL). Each viewer's
findNotebookUrlfalls back toextractNotebookUrlMarkerto pull the URL out locally — no server round-trip — and strips the whole<internal>...</internal>block from the display so the user never sees it.Why two commits
The first commit implemented this via a server round-trip: an opaque
<nbid>id in the content, resolved through an app-initiatedresources/read. That turned out not to work on the target hosts — a host that drops_metaalso does not forward appresources/read, answering with JSON-RPC-32601 "Method not found". The second commit pivots to the self-contained marker above and removes the resolver. The history is kept intentionally to record what was tried.Changes
Kernel/UIResources.wl):makeNotebookUIResultbuilds the result and appends the URL marker for cloud URLs (inline notebooks are delivered via_metaonly).evaluator-viewer,wolframalpha-viewer,notebook-viewer):findNotebookUrl→extractNotebookUrlMarkerfallback;stripAgentOnlyTextremoves the marker;notebook-vieweruses it as a result-path fallback (it normally gets its URL from the tool input).Tests/MCPApps.wlt) and docs (docs/mcp-apps.md) updated.Test plan
Tests/MCPApps.wlt,Tests/WolframAlpha-UI.wlt,Tests/WolframLanguageEvaluator-UI.wlt,Tests/EvaluatorSessions.wlt— all pass (151/151 in-process)._meta.notebookUrl; marker stripped from the user-facing display; real output preserved.CodeInspectorclean onUIResources.wl._meta(e.g. Claude Desktop) that the notebook now embeds. (Theresources/readfailure this replaces was observed there; the marker path depends only on tool content being delivered, which is.)Note:
Tests/StartMCPServer.wltspawnswolframscriptsubprocesses and can't run in the sandbox used here; it should run in CI.🤖 Generated with Claude Code
https://claude.ai/code/session_015Y8TNLJ63zJor51rq99TqQ