Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Then install the checkout in your app with `pnpm add /path/to/webmcp-polyfill`.

## Usage

Use HTTPS or localhost. Some browsers need an `Origin-Agent-Cluster: ?1` header; current Chrome enables origin keying by default.
Use HTTPS or localhost.

Load the polyfill before registering tools:

Expand Down Expand Up @@ -75,8 +75,6 @@ import "webmcp-polyfill/auto";

Next.js runs [client instrumentation](https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation-client) before hydration. A Server Component import alone won't install the polyfill in the browser.

Configure `Origin-Agent-Cluster` through Next.js [`headers()`](https://nextjs.org/docs/app/api-reference/config/next-config-js/headers) if your browser needs it.

Both entry points are SSR-safe: installation does nothing without a document. Your pages can stay server-rendered; WebMCP runs in the browser.

The package needs no `"use client"` directive. Use it on your [Client Components](https://nextjs.org/docs/app/api-reference/directives/use-client) and access `document.modelContext` in effects or event handlers. Pass an `AbortSignal` when registering in an effect, then abort it during cleanup.
Expand Down
49 changes: 21 additions & 28 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,29 @@

## Results

**226 browser tests pass** across Chromium 153.0.8010.12, Firefox 155.0, and
**223 browser tests pass** across Chromium 153.0.8010.12, Firefox 155.0, and
Playwright WebKit 26.6. Package checks also pass: imports, types, SSR, and tarball contents.
CI runs these checks plus WPT in Chrome, Firefox, and actual Safari. Safari runs on
`macos-26`; Playwright WebKit is a separate build.

WPT covers **all 67 WebMCP testharness files, 161 subtests**, with zero unexpected results:
WPT covers **all 70 WebMCP testharness files, 190 subtests**, with zero unexpected results
in Chrome and Firefox:

| Subtest result | Chrome / Firefox | Safari |
| --- | ---: | ---: |
| PASS | 96 | 92 |
| Expected FAIL | 23 | 27 |
| Expected TIMEOUT | 24 | 24 |
| Expected NOTRUN | 18 | 18 |
| Subtest result | Count |
| --- | ---: |
| PASS | 92 |
| Expected FAIL | 52 |
| Expected TIMEOUT | 26 |
| Expected NOTRUN | 20 |

Tested with Chrome Canary 156.0.8068.0, Firefox Nightly 158.0a1 (20260922211342),
Firefox 142.0.1, and Safari 26.6.2 (21624.5.1.11.3) on macOS 26.6.2.
At the file level: 42 OK, 24 expected timeouts, one expected error.
Tested with Chrome Canary 157.0.8080.0 and Firefox Nightly 159.0a1 (20260930214513).
Safari has not yet run at this pin; none of the expectations are browser-specific.
At the file level: 43 OK, 26 expected timeouts, one expected error.

Expected failures are still failures. `NOTRUN` means an earlier timeout prevented
the test from running, including three abort cases. Passing declarative checks only
cover rejection or absence of tools. All 22 pinned IDL checks pass, but that IDL
predates `debugging` and lifecycle events. This is not full conformance.
cover rejection or absence of tools. Of the 38 pinned IDL checks, the 16 for lifecycle
event handlers and interfaces fail. This is not full conformance.

## Run locally

Expand All @@ -41,7 +42,7 @@ native WebMCP. A separate Chromium test uses `--enable-features=WebMCP` to check
that loading the polyfill preserves the native context and its tools.

WPT needs Python 3.11+ and a clean checkout at
[`2699eaa`](https://github.com/web-platform-tests/wpt/commit/2699eaa2e5f906eda4d7443f7080c3de19e62365).
[`fe52996`](https://github.com/web-platform-tests/wpt/commit/fe52996d4465f23617bce91927bdd58e6ce8f541).
The [CI workflow](.github/workflows/test.yml) has the sparse-checkout and dependency setup.

```sh
Expand All @@ -50,10 +51,7 @@ WPT_ROOT=../wpt WPT_BROWSER=firefox pnpm test:wpt
WPT_ROOT=../wpt WPT_BROWSER=safari pnpm test:wpt
```

Firefox downloads Nightly unless `FIREFOX_BIN` is set. The runner enables
`dom.origin_agent_cluster.default` in the test profile because the pinned pages
assume origin keying without a header; [Firefox defaults to site keying](https://github.com/mozilla-firefox/firefox/blob/FIREFOX_142_0_1_RELEASE/modules/libpref/init/StaticPrefList.yaml#L5768-L5773).
Local fixtures test explicit `Origin-Agent-Cluster` headers.
Firefox downloads Nightly unless `FIREFOX_BIN` is set.
Safari requires macOS, [Remote Automation, and WPT hosts-file setup](https://web-platform-tests.org/running-tests/safari.html).

Set `WPT_PYTHON` or `WPT_VENV` to use an existing Python environment. Extra arguments
Expand All @@ -67,23 +65,18 @@ other non-testharness files are outside this suite.

## Draft alignment and limitations

Checked against [draft `f5645e9`](https://github.com/webmachinelearning/webmcp/blob/f5645e9aea51eb589599f181d104a2f49430608e/index.bs)
Checked against [draft `d61d0e6`](https://github.com/webmachinelearning/webmcp/blob/d61d0e6d297ddb6bff3510b1330dbb215c6ef43c/index.bs)
and `webmcp-types@0.1.9`.

- **Missing APIs:** declarative forms, CSS states, and lifecycle events are not
implemented. This includes both the draft's `ModelContext` events and the older
`window` events WPT expects.
- **Missing APIs:** declarative forms, CSS states, and lifecycle events
(`toolactivated`/`toolcancel`, their handlers, and `ToolActivatedEvent`/`ToolCancelEvent`)
are not implemented.
- **Draft differences:** results are JSON-serialized; some pinned tests expect raw
strings. Omitted or `undefined` input becomes `{}`, matching the types and pinned
IDL rather than the recorded draft. `null` and primitives reject.
strings. Omitted or `undefined` input becomes `{}`; `null` and primitives reject.
- **Timing:** MessagePorts approximate native task ordering. Aborting before
dispatch skips the callback; the draft dispatches and then aborts its signal.
Delegated permission checks are asynchronous, so argument errors can precede
`NotAllowedError`.
- **Safari:** the tested version lacks `originAgentCluster`, so the polyfill skips
that check. Four WPT assertions fail with `NotAllowedError` instead of
`SecurityError`. [WebKit's implementation](https://github.com/WebKit/WebKit/pull/66162)
landed behind a flag.
- **Frames:** each participating document must load the polyfill. Native contexts
and WPT's uninstrumented blank helper documents cannot answer its messages.
Opaque-origin handshakes and inaccessible cross-origin shadow frames are
Expand Down
3 changes: 0 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,9 +607,6 @@ function requireActiveWindow(owner: Document): Window {
if (!view) {
throw new NativeDOMException("The document is not fully active", "InvalidStateError");
}
if (view.originAgentCluster === false && view.location.protocol !== "file:") {
throw new NativeDOMException("An origin-keyed agent cluster is required", "SecurityError");
}

// Synchronous where the browser exposes the policy; FrameBridge.allowed() covers the rest.
if (readToolsPolicy(owner) === false) {
Expand Down
2 changes: 0 additions & 2 deletions tests/fixtures/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const frameHtml = '<!doctype html><title>WebMCP frame</title><script src="/auto.
const pages: Record<string, string> = {
"/": blankHtml,
"/health": blankHtml,
"/no-cluster": blankHtml,
"/app": appHtml,
"/frame": frameHtml,
"/startup": startupHtml,
Expand All @@ -54,7 +53,6 @@ const pages: Record<string, string> = {

createServer(async (request, response) => {
const path = new URL(request.url ?? "/", "http://localhost").pathname;
response.setHeader("Origin-Agent-Cluster", path === "/no-cluster" ? "?0" : "?1");
response.setHeader("Cache-Control", "no-store");
const page = pages[path];

Expand Down
33 changes: 0 additions & 33 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,39 +41,6 @@ test("operations reject invalid receivers before reading arguments", async ({ pa
expect(outcome).toEqual({ reads: 0, errors: Array(12).fill("TypeError") });
});

test("every operation rejects when the server opts out of origin-keyed agent clustering", async ({
page,
}) => {
await page.goto("http://127.0.0.1:8793/no-cluster");
await page.addScriptTag({ url: "/auto.js" });
const outcome = await page.evaluate(async () => {
const context = document.modelContext!;
const errors = [];
for (const operation of [
() => context.getTools(),
() => context.registerTool({ name: "x", description: "X", execute: () => null }),
() =>
context.executeTool(
{ name: "x", title: "", description: "X", window, origin: location.origin },
{},
),
]) {
try {
await operation();
errors.push("resolved");
} catch (error) {
if (!(error instanceof Error)) {
throw error;
}
errors.push(error.name);
}
}
return errors;
});

expect(outcome).toEqual(["SecurityError", "SecurityError", "SecurityError"]);
});

test("origin filters accept only potentially trustworthy origins", async ({ page }) => {
await page.addScriptTag({ url: "/auto.js" });
const outcome = await page.evaluate(async () => {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Declarative form registration is unsupported; waitForTool() never resolves.
[executeTool-detach-toolactivated.https.html]
expected: TIMEOUT
[toolactivated is not dispatched when filling out a declarative tool form detaches the tool document]
expected: TIMEOUT
35 changes: 35 additions & 0 deletions wpt/metadata/webmcp/idlharness.https.window.js.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Lifecycle events are unsupported: ModelContext has no ontoolactivated/ontoolcancel
# handlers, and ToolActivatedEvent/ToolCancelEvent are not defined.
[idlharness.https.window.html]
[ModelContext interface: attribute ontoolactivated]
expected: FAIL
[ModelContext interface: attribute ontoolcancel]
expected: FAIL
[ToolActivatedEvent interface: existence and properties of interface object]
expected: FAIL
[ToolActivatedEvent interface object length]
expected: FAIL
[ToolActivatedEvent interface object name]
expected: FAIL
[ToolActivatedEvent interface: existence and properties of interface prototype object]
expected: FAIL
[ToolActivatedEvent interface: existence and properties of interface prototype object's "constructor" property]
expected: FAIL
[ToolActivatedEvent interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
[ToolActivatedEvent interface: attribute toolName]
expected: FAIL
[ToolCancelEvent interface: existence and properties of interface object]
expected: FAIL
[ToolCancelEvent interface object length]
expected: FAIL
[ToolCancelEvent interface object name]
expected: FAIL
[ToolCancelEvent interface: existence and properties of interface prototype object]
expected: FAIL
[ToolCancelEvent interface: existence and properties of interface prototype object's "constructor" property]
expected: FAIL
[ToolCancelEvent interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
[ToolCancelEvent interface: attribute toolName]
expected: FAIL

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# WPT injection does not install the polyfill in /common/blank.html helper frames, so
# their document.modelContext is undefined.
[executeTool-detach-toolactivated.https.html]
[toolactivated is not dispatched when a tool execute callback from another realm detaches the tool document]
expected: FAIL
[toolactivated is not dispatched when a tool execute callback detaches its own document]
expected: FAIL
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# WPT injection does not install the polyfill in /common/blank.html helper frames, so
# their document.modelContext is undefined.
[executeTool-detach-toolcancel.https.html]
[toolcancel is not dispatched when a tool abort handler from another realm detaches the tool document]
expected: FAIL
[toolcancel is not dispatched when a tool abort handler detaches its own document]
expected: FAIL
10 changes: 7 additions & 3 deletions wpt/metadata/webmcp/imperative/executeTool-events.https.html.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Lifecycle events are unsupported. This test waits for window events; the current
# draft places toolactivated/toolcancel on ModelContext.
# Lifecycle events are unsupported. The first case waits for a toolactivated event
# that is never dispatched, so the later cases do not run.
[executeTool-events.https.html]
expected: TIMEOUT
[toolactivated and toolcancel events are dispatched on window during tool execution and cancellation]
[toolactivated and toolcancel events are dispatched on document.modelContext during tool execution and cancellation]
expected: TIMEOUT
[toolactivated is dispatched at the ModelContext of the tool target document]
expected: NOTRUN
[toolcancel is dispatched at the ModelContext of the tool target document]
expected: NOTRUN
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# The draft rejects a removed target with UnknownError; the test expects InvalidStateError.
# The draft rejects a removed target with UnknownError; the first case expects
# InvalidStateError. The last two cases register in /common/blank.html helper frames,
# where WPT injection does not install the polyfill.
[executeTool-target-detachment.https.html]
[executeTool() rejects when target frame is detached before execution]
expected: FAIL
[executeTool() rejects cleanly when tool callback synchronously detaches its own frame]
expected: FAIL
[executeTool() abort cleanly handles synchronous frame detachment inside options.signal abort listener]
expected: FAIL
11 changes: 11 additions & 0 deletions wpt/metadata/webmcp/tool-activated-event.https.html.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Lifecycle events are unsupported: ToolActivatedEvent is not defined and executeTool()
# dispatches no toolactivated event.
[tool-activated-event.https.html]
[ToolActivatedEvent constructor with default event init dictionary]
expected: FAIL
[ToolActivatedEvent constructor with an event init dictionary]
expected: FAIL
[ontoolactivated is invoked for synthetic toolactivated events dispatched at ModelContext]
expected: FAIL
[ontoolactivated is invoked when a tool is activated]
expected: FAIL
12 changes: 12 additions & 0 deletions wpt/metadata/webmcp/tool-cancel-event.https.html.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Lifecycle events are unsupported: ToolCancelEvent is not defined, and the last case
# waits for a toolcancel event that is never dispatched.
[tool-cancel-event.https.html]
expected: TIMEOUT
[ToolCancelEvent constructor with default event init dictionary]
expected: FAIL
[ToolCancelEvent constructor with an event init dictionary]
expected: FAIL
[ontoolcancel is invoked for synthetic toolcancel events dispatched at ModelContext]
expected: FAIL
[ontoolcancel is invoked when a tool invocation is canceled]
expected: TIMEOUT
2 changes: 1 addition & 1 deletion wpt/revision.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2699eaa2e5f906eda4d7443f7080c3de19e62365
fe52996d4465f23617bce91927bdd58e6ce8f541
6 changes: 3 additions & 3 deletions wpt/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ switch (browser) {
}
case "firefox": {
const binary = process.env.FIREFOX_BIN;
browserArguments.push("--headless", "--setpref=dom.origin_agent_cluster.default=true");
browserArguments.push("--headless");
browserArguments.push(
...(binary
? ["--channel=stable", "--binary", binary]
Expand Down Expand Up @@ -114,11 +114,11 @@ const subtests = results.flatMap((fileResult) => fileResult.subtests);
const uniqueFiles = new Set(results.map((fileResult) => fileResult.test));

// These counts belong to the pinned revision; an incomplete run must not pass.
assert.equal(results.length, 67, "Expected all 67 WebMCP testharness files at the pin");
assert.equal(results.length, 70, "Expected all 70 WebMCP testharness files at the pin");
assert.equal(uniqueFiles.size, results.length, "WPT repeated a test file");
assert.equal(
subtests.length,
161,
190,
"WPT subtest count changed; inspect the report and expectations",
);
const statusCounts = { PASS: 0, FAIL: 0, TIMEOUT: 0, NOTRUN: 0, PRECONDITION_FAILED: 0 };
Expand Down
Loading