Skip to content

Scope ModelContext to Document, and handle detached documents - #177

Merged
domfarolino merged 2 commits into
mainfrom
fix-modelcontext-lifetime
May 19, 2026
Merged

domfarolino merged 2 commits into
mainfrom
fix-modelcontext-lifetime

Conversation

@domfarolino

@domfarolino domfarolino commented May 15, 2026 •

Copy link
Copy Markdown
Collaborator

This PR does two things:

  • Scopes the ModelContext object to Document instead of Navigator, and explains why.
  • Makes the modelContext attribute return null when the document is detached (by checking when the Window's browsing context is null). This matches the behavior of the contentWindow and contentDocument getters—return nothing, even if those objects are allocated and kept alive by other references, in the detached document case.

Note that this PR does not handle the "document not fully active" case, which covers detached documents and documents in the bf-cache. Non-fully active documents probably shouldn't impact the modelContext getter—it seems weird for getters like that to return different values based on whether the document is in the bf-cache—but rather need special behavior in the registerTool() method, and the coming getTools() and executeTool() methods.

This PR mostly addresses #173, with the exception of considering moving the API to self, from navigator. As much as I want to do this, I'm sympathetic to w3ctag/design-principles#426 (comment), so for now I'm leaning towards not moving it. So I'll say that this PR closes #173.


Preview | Diff

@domfarolino
domfarolino requested a review from bwalderman May 15, 2026 23:33
Comment thread index.bs
@domfarolino domfarolino changed the title Scope and null-out for detached documents Tie ModelContext lifetime to Document and handle detached documents May 15, 2026
@domfarolino domfarolino changed the title Tie ModelContext lifetime to Document and handle detached documents Scope ModelContext to Document, and handle detached documents May 15, 2026
Comment thread index.bs Outdated
Comment thread index.bs Outdated
@domfarolino
domfarolino requested a review from bwalderman May 18, 2026 20:25
@domfarolino
domfarolino merged commit 986f461 into main May 19, 2026
2 checks passed
@domfarolino
domfarolino deleted the fix-modelcontext-lifetime branch May 19, 2026 02:19
github-actions Bot added a commit that referenced this pull request May 19, 2026
SHA: 986f461
Reason: push, by domfarolino

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
rgarcia added a commit to kernel/kernel-images that referenced this pull request Sep 28, 2026
)

## Summary

Sites that shipped WebMCP tools before Chromium exposed the native
registry install their own polyfill on `navigator.modelContext`.
Chromium 150 deprecated that alias and later removed it in favor of
`document.modelContext` (spec: webmachinelearning/webmcp#177). A
polyfill that does not forward to the native registry is invisible to
the CDP WebMCP domain, so `GET /webmcp/tools` and `webmcp.listTools()`
list nothing even though `await navigator.modelContext.listTools()`
shows the tools in page context.

This change copies those tools into the frame's native
`document.modelContext` registry. From there the CDP WebMCP domain
lists, invokes, and removes them like any other page tool; the API and
result shapes are unchanged.

- On every tool listing, `webmcpclient` syncs a bridge
(`polyfill_bridge.js`) in the main world of each tracked frame. Root
frames are reached through their session; same-process child frames
through the owner element's `contentWindow`.
- The bridge is created with `Runtime.callFunctionOn` and is reachable
only through the client's remote object handle, so no globals or
prototypes change on the page, and neither `Runtime.enable` nor
`DOM.enable` is used. Pages that call `document.modelContext.getTools()`
do see the bridged tools, the same as custom page tools today.
- Each sync registers new polyfill tools, unregisters removed or changed
ones, and resets when a same-origin child frame navigates. Bridged tools
register under a reserved `polyfill.` prefix, which discovery strips, so
they never occupy a name the page may register natively later. When the
page registers a name natively, before or after bridging, the native
tool is listed and the bridge withdraws its copy on the next sync. A
`navigator.modelContext` that is the native registry itself is skipped.
- Recognized polyfill conventions: `listTools()` / `getTools()` (array
or `{tools}`), registries on `_registeredTools`, `_tools`, `tools`, or
`window.__webmcp.tools`, and execution through a registry entry's
`execute`, `callTool(name, input)`, `callTool({name, arguments})`, or
`executeTool(name, json)`. Metadata is copied as JSON with per-tool,
total-list, and output size bounds.
- `browsersurface` now records which session owns each frame and exposes
`Tracker.SessionFrames`.
- A page tool that throws now reports its message in `error_text`.
Chromium attaches the thrown value to `WebMCP.toolResponded` as
`exception` and leaves `errorText` empty; the client previously dropped
it for every page tool.
- Tool references are released by registered name when a document goes
away, so a replaced document never reuses a stale reference for a
prefixed (custom or bridged) tool.

## Testing

- `go test -race ./lib/webmcpclient/ ./lib/browsersurface/
./cmd/api/api/`: fake-CDP tests cover bridging into root, same-process
child, and out-of-process frames, bridge reuse, unregistration,
recreation for a new document, native precedence and fallback,
invocation under the registered name, exception text, and that
`Runtime.enable` / `DOM.enable` are never sent.
- `node --test runtime/webmcp-polyfill.test.ts` and `make
runtime-typecheck`: runs the bridge against site-style, MCP-style, and
registry-only polyfills, native collisions, malformed entries, output
bounds, and document replacement.
- `TestPlaywrightExecuteAPI` e2e against a locally built headless image:
the existing declarative and custom subtests plus the new
`WebMCPPolyfill` subtest (late-installing polyfill, `srcdoc` child
frame, native precedence, a native registration of a bridged name after
bridging, thrown-error text, unregistration, navigation, stale refs).

---------

Co-authored-by: rgarcia <72655+rgarcia@users.noreply.github.com>
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.

Make tools Document-scoped instead of Window-scoped

4 participants