Skip to content

Prototype WebMCP for View-exposed tools, to illustrate #797 - #798

Draft
ochafik wants to merge 7 commits into
modelcontextprotocol:mainfrom
ochafik:webmcp-soft-deprecate-app-tools
Draft

ochafik wants to merge 7 commits into
modelcontextprotocol:mainfrom
ochafik:webmcp-soft-deprecate-app-tools

Conversation

@ochafik

@ochafik ochafik commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Prototype of one option discussed in #797: make WebMCP (document.modelContext) how a View exposes tools, deprecating app.registerTool and View-handled tools/list. The issue is undecided, so this is meant to make that option concrete for discussion, not to merge as-is. Related: #796, #691.

AppBridge injects a small polyfill into the View HTML (a native implementation wins). The sandbox proxy, the only party same-origin with the View, reads the tools with a new createModelContextRelay and answers reserved messages; Hosts call new listWebMcpTools() / callWebMcpTool(). Results are {content: [], structuredContent: value}. Spec, basic-host, four examples and a v3 migration guide follow. The last commit (docker e2e script) is independent and can be dropped.

Breaking: a View using registerTool exposes no tools on a Host without the injection and relay. It needs a major release; no version bump here.

Test plan

bun test src examples passes (558); tsc and typedoc are clean. npm run test:e2e:docker shows the same 24 failures as a clean origin/main run (font rendering on arm64 Docker, PDF servers not starting) plus 3 new passing WebMCP tests, which also pass in Chrome 154 with native WebMCP. Not verified: other browsers, real third-party Hosts.

Known limits: no image results, no size caps in the relay, native quirks handled for current Chrome only.

cc/ @liady @idosal @domfarolino

Adds ui/sandbox-list-tools, ui/sandbox-call-tool and
ui/notifications/sandbox-tools-changed to the draft specification: the
Sandbox proxy, which is the only party same-origin with the View, reads
the tools the View registers with WebMCP (document.modelContext) and
answers the Host itself. The standard tools/list and tools/call are
untouched and keep reaching Views that answer them.

The `tools` app capability and the View-handled tools/list, tools/call
and notifications/tools/list_changed are marked deprecated in favor of
WebMCP. Includes the matching spec types and regenerated schemas.

Illustrates one of the options discussed in modelcontextprotocol#797.
App.registerTool() becomes a deprecated wrapper over
document.modelContext.registerTool(): the App no longer serves the tool
over tools/list / tools/call and no longer declares the `tools`
capability on its behalf. A callback's structuredContent (or its
content) becomes the WebMCP result; isError rejects. App.oncalltool,
App.onlisttools and App.sendToolListChanged are deprecated too, and the
first two warn once per App.

Host side, AppBridge.sendSandboxResourceReady() injects a small
document.modelContext polyfill into the View HTML by default
(`modelContextPolyfill: false` opts out; a native implementation wins).
createModelContextRelay() is the helper a Sandbox proxy uses to read the
View's tools and answer the new reserved messages, and AppBridge gains
listWebMcpTools(), callWebMcpTool() and onwebmcptoolschange. listTools()
and callTool() are deprecated.

A tool result is {content: [], structuredContent: value}; a tool that
throws is an isError result.

This is a breaking change in behavior: a View on this SDK that uses
registerTool exposes no tools on a Host that has not adopted the
polyfill injection and the relay.
The sandbox proxy uses createModelContextRelay() to answer the Host's
reserved tool messages from the View's document.modelContext, and the
Host lists and calls the tools with listWebMcpTools() / callWebMcpTool()
in a new "App tools (WebMCP)" panel. The sandbox iframe delegates the
`tools` Permissions Policy feature so native WebMCP works too.
Migrates the budget-allocator, map, pdf and debug examples from
app.registerTool() to document.modelContext.registerTool(), typed with
webmcp-types (0.1.10, MIT, no dependencies, pinned). Tools return plain
data, which reaches the Host as structuredContent, and throw to report
errors. debug-server logs its WebMCP calls instead of the raw
oncalltool / onlisttools handlers; three.js no longer declares a `tools`
capability it has no handlers for.
Describes the move to WebMCP for Views, and what Hosts and Sandbox
proxies have to adopt, including the behavior change for Views that
keep using app.registerTool() on a Host without the polyfill injection
and the relay.
Loads the budget-allocator, debug and shadertoy examples in basic-host
and lists and calls their tools end to end: a View registering with
document.modelContext, a throwing tool, and the deprecated
App.registerTool() wrapper (with its single warning).
The docker targets bind-mounted the repo and ran `npm ci` in it, which
left the host with Linux binaries in node_modules, and passed `-it`,
which fails without a terminal. They now share scripts/e2e-docker.sh:
the repo is still mounted (so snapshots are written back) but every
node_modules directory is shadowed by an anonymous volume, and `-it` is
only used when stdin and stdout are terminals.
@pkg-pr-new

pkg-pr-new Bot commented Sep 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/@modelcontextprotocol/ext-apps@798

@modelcontextprotocol/server-basic-preact

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-preact@798

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-react@798

@modelcontextprotocol/server-basic-solid

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-solid@798

@modelcontextprotocol/server-basic-svelte

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-svelte@798

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vanillajs@798

@modelcontextprotocol/server-basic-vue

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vue@798

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/@modelcontextprotocol/server-budget-allocator@798

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/@modelcontextprotocol/server-cohort-heatmap@798

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/@modelcontextprotocol/server-customer-segmentation@798

@modelcontextprotocol/server-debug

npm i https://pkg.pr.new/@modelcontextprotocol/server-debug@798

@modelcontextprotocol/server-lazy-auth

npm i https://pkg.pr.new/@modelcontextprotocol/server-lazy-auth@798

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/@modelcontextprotocol/server-map@798

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/@modelcontextprotocol/server-pdf@798

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/@modelcontextprotocol/server-scenario-modeler@798

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/@modelcontextprotocol/server-shadertoy@798

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/@modelcontextprotocol/server-sheet-music@798

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/@modelcontextprotocol/server-system-monitor@798

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/@modelcontextprotocol/server-threejs@798

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/@modelcontextprotocol/server-transcript@798

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/@modelcontextprotocol/server-video-resource@798

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/@modelcontextprotocol/server-wiki-explorer@798

commit: efcc19c

@kziemski

kziemski commented Oct 6, 2026

Copy link
Copy Markdown

shouldn't this fail always in chrome that has enabled the webmcp flag?

@kenchris

Copy link
Copy Markdown

Small question while implementing the host side of this in our extension: is cursor / nextCursor on ui/sandbox-list-tools meant to be used?

As far as I can read the diff, the relay's handler for that message ignores the params and returns everything from one getTools() call ({ result: { tools: tools.map(({ mcp }) => mcp) } }), so nextCursor is never set. listWebMcpTools() on the host side sends one request with whatever cursor it was given and returns the reply, with no loop. And getTools() has no way to ask for a page anyway. So as written the two fields look like they came along with ListToolsResultSchema.

If that's the intent, it would help to say so in the spec text, or to leave the fields out of the message, so a host author doesn't wonder whether a reply without nextCursor could still hide more tools. If it's not the intent, I'd like to know what a relay is supposed to do with it. We just return all tools in one reply and cap them on the host.

I'm also happy to look at paging for getTools() on the WebMCP side if this needs it, but I don't think anything needs it today.

@kenchris

Copy link
Copy Markdown

One more thing from the host side: I can't see a way to cancel a ui/sandbox-call-tool.

The relay calls consumer.executeTool(found.tool, input) with two arguments, so no signal reaches the tool, even though executeTool takes options.signal. A host that gives up after a timeout, or because the user stopped the turn, can't tell the View, and a tool that never settles just keeps running there. We ignore a late answer on our side, but that's all we can do.

MCP already has notifications/cancelled with a request id, and I think that would fit: the relay keeps an AbortController per request that is still running and aborts it when the notification comes. Is that something you'd want in this PR, or is it for later? Happy to try it against our host if it helps.

@kenchris

Copy link
Copy Markdown

A small data point on the string-or-object input question, in case it's useful. I tried it on Chrome 155 with native WebMCP: getTools() reports inputSchema as an object (and undefined for a tool without one), executeTool(tool, '{}') is rejected with invalid input object: value is not an object, and executeTool(tool, {}) works.

So the comment in the relay saying Chrome reports schemas as JSON strings and takes the input as one is out of date for 155. Nothing breaks, because the rule it actually uses (string input only when some schema is a string) gives the object here. It might be worth updating the comment so nobody later thinks the string form is what current Chrome wants.

This branch has not been deployed

No deployments
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.

3 participants