Skip to content
Draft
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
93 changes: 93 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: Test

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm install --global pnpm@10.14.0
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps chromium firefox webkit
- run: pnpm test
- run: pnpm test:package
- uses: actions/upload-artifact@v7
if: always()
with:
name: browser-tests
path: playwright-report/
retention-days: 7

wpt:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Read WPT revision
id: wpt
run: echo "revision=$(cat wpt/revision.txt)" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v7
with:
repository: web-platform-tests/wpt
ref: ${{ steps.wpt.outputs.revision }}
path: .reference/wpt
persist-credentials: false
sparse-checkout: |
common
docs
interfaces
resources
tools
webmcp
- uses: actions/setup-node@v7
with:
node-version: 24
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- run: npm install --global pnpm@10.14.0
- run: pnpm install --frozen-lockfile
- uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd
id: chrome
with:
chrome-version: canary
- name: Allow Chrome to create its sandbox
env:
CHROME_BIN: ${{ steps.chrome.outputs.chrome-path }}
run: |
sudo tee /etc/apparmor.d/webmcp-chrome >/dev/null <<EOF
abi <abi/4.0>,
profile webmcp-chrome "$CHROME_BIN" flags=(unconfined) {
userns,
}
EOF
sudo apparmor_parser -r /etc/apparmor.d/webmcp-chrome
- run: pnpm test:wpt
env:
WPT_ROOT: .reference/wpt
CHROME_BIN: ${{ steps.chrome.outputs.chrome-path }}
- uses: actions/upload-artifact@v7
if: always()
with:
name: wpt-results
path: wpt-results/
retention-days: 7
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules/
dist/
test-results/
playwright-report/
*.tgz
wpt-results/
wpt/metadata/MANIFEST.json
wpt/metadata/.cache/
.reference/
10 changes: 10 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"plugins": ["typescript", "unicorn"],
"categories": {
"correctness": "error"
},
"rules": {
"typescript/no-explicit-any": "error",
"eslint/no-shadow": "error"
}
}
45 changes: 45 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Working on the polyfill

This package implements the document-local imperative WebMCP draft. Use the
official `webmcp-types` dependency; do not duplicate its declarations.
No MCP server, transport, extension product, navigator aliases, or legacy API
compatibility belongs here.

## Before changing behavior

1. Read the live [Community Group draft](https://webmachinelearning.github.io/webmcp/)
and the diff from the source revision recorded in TESTING.md.
2. Read the relevant upstream WPT, including its helpers and IDL. The pin is in
`wpt/revision.txt`, selection in `wpt/run.ts`, and disagreements in TESTING.md.
3. Check browser evidence using the source map in TESTING.md. A Chromium test,
browser issue, or standards-position discussion alone is not the specification.
4. Add the smallest real-browser regression that demonstrates the behavior.
Tests load the built bundle from a real server; do not replace DOM APIs,
page requests, or tool callbacks with mocks.

## Validate

Run `pnpm test`, `pnpm test:package`, and `pnpm test:wpt`. TESTING.md has the
prerequisites and says what each suite covers. Type checking runs over the test
code and the published declarations, not just `src/index.ts`. A missing browser or
driver must fail its project, never become a skipped test. Do not count an
excluded test as a pass, and do not add compatibility behavior only to satisfy
one.

When updating the WPT revision, read the upstream diff first, update the coverage
counts in `wpt/run.ts`, and inspect every changed assertion and expectation. Do not
regenerate failure metadata without reading each one. In the pull request
description, record the draft and WPT revisions, browser versions, passes,
expected failures, and excluded behavior separately. Keep upstream tests
unmodified and do not claim full conformance.

## Code

Favor a clear reading order over minimum line count. Use braces, name intermediate
values by their role, and keep argument conversion separate from tool operations.
Put the public entry point and operations before their lower-level helpers.

Web IDL conversion accepts unknown values and broad objects, and feature
detection needs runtime checks. Do not narrow either one to satisfy a lint rule.
Keep casts at conversion boundaries, state the invariant each one checks, and use
the upstream types and concrete return types inside the implementation.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 WebMCP polyfill contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# WebMCP polyfill

A polyfill for [WebMCP](https://webmachinelearning.github.io/webmcp/), with types from [webmcp-types](https://github.com/webmachinelearning/webmcp-types) and no runtime JavaScript dependencies.

## Build

This package is in development. Build this checkout with Node.js 24 and pnpm:

```sh
pnpm install
pnpm build
```

Then install the checkout in your app with `pnpm add /path/to/webmcp-polyfill`. For a classic script, serve the built `dist/polyfill.js`.

## Usage

Serve your page over HTTPS, or localhost HTTP for development. Origin-keyed agent clustering must be enabled; current Chrome enables it by default, so an `Origin-Agent-Cluster: ?1` header is optional.

Load the polyfill before registering tools:

```ts
import "webmcp-polyfill/auto";

const context = document.modelContext;
if (!context) {
throw new Error("WebMCP requires a secure browser context");
}

const registration = new AbortController();
await context.registerTool(
{
name: "page-title",
description: "Get the title of this page",
execute() {
return { title: document.title };
},
},
{ signal: registration.signal },
);

const tools = await context.getTools();
const pageTitleTool = tools.find((tool) => tool.name === "page-title");
if (!pageTitleTool) {
throw new Error("The page-title tool is unavailable");
}

const result = await context.executeTool(pageTitleTool, {});
console.log(result);

// Remove the tool when it is no longer needed.
registration.abort();
```

For explicit installation, import and call `installWebMCP` from `webmcp-polyfill`. It is safe to call repeatedly and during server-side rendering. Existing `document.modelContext` implementations are preserved, including partial native implementations. Each frame installs separately.

## Scope

Tools stay in the current document. Cross-document tools, declarative forms, lifecycle window events, and browser agent integration aren't implemented. Nonempty `exposedTo` and `fromOrigins` options reject.

`executeTool()` accepts an object and returns a JSON-serialized result. Callbacks must validate their inputs; schema inference provides TypeScript checks only.

The implementation tracks the [Community Group draft](https://webmachinelearning.github.io/webmcp/). [TESTING.md](https://github.com/webmachinelearning/webmcp-polyfill/blob/main/TESTING.md) records the draft and WPT revisions, test coverage, and known limitations.

Breaking API changes ship with notes: in minor releases while the version is 0.x, in majors after 1.0.

## Development

`src/` contains the polyfill and its automatic entry point. `tests/` contains the browser and package checks with their fixtures. `wpt/` contains the upstream test runner, pinned revision, and expectations.

See [TESTING.md](https://github.com/webmachinelearning/webmcp-polyfill/blob/main/TESTING.md) for browser setup and test commands.

## License

[MIT](LICENSE).
126 changes: 126 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Testing and upstream tracking

## Browser and package checks

Use Node.js 24 and pnpm:

```sh
pnpm install --frozen-lockfile
pnpm exec playwright install --with-deps chromium firefox webkit
pnpm test
pnpm test:package
```

`pnpm test` runs lint, builds the bundle, checks TypeScript, and runs Playwright.
Tests load the built bundle from a real server in Chromium, Firefox, and WebKit.
Chromium runs with native WebMCP disabled; a separate native Chromium test checks
that installation preserves its context and registered tools.
Browser tests in `tests/*.test.ts` are discovered automatically.
`tests/native.test.ts` runs only in the native Chromium project;
`tests/package.test.ts` runs separately through `pnpm test:package`.
All source, tests, and Node scripts are type-checked, including the browser
fixture JavaScript through `checkJs`. Node 24 runs the TypeScript scripts directly;
they use erasable syntax and need no separate compilation step.

| File | Coverage |
| ----------------------- | ------------------------------------------------------------------------------------------- |
| `tests/index.test.ts` | Registration, discovery, conversion, metadata copies, events, abort, and detached documents |
| `tests/execute.test.ts` | Object input, JSON results, cancellation, concurrent calls, and dispatch failures |
| `tests/app.test.ts` | Button interactions, callback side effects, invalid input, unregistration, and reload |
| `tests/native.test.ts` | Preservation of the native context, getter, and tools |
| `tests/index.test-d.ts` | Published declarations and upstream schema inference |
| `tests/package.test.ts` | Packed consumer imports, type inference, SSR entry points, and package contents |

The fixture server, `tests/fixtures/server.ts`, uses port 8793 and loopback HTTP, which is a secure context.
It sends `Origin-Agent-Cluster: ?1` for consistent setup and `?0` on the opt-out
fixture. The header is optional in current Chrome's default configuration.
Playwright requires a free port and retains failure traces. Its bundled WebKit
provides engine coverage; it is not Safari.

## Upstream WPT

Use Python 3.11+, Chrome Canary, and a clean WPT checkout at
[`1a21db90adf8a264370ad806ed761f39e1d435a0`](https://github.com/web-platform-tests/wpt/commit/1a21db90adf8a264370ad806ed761f39e1d435a0).
CI and the local runner both read the pin from `wpt/revision.txt`.
A sparse checkout needs `common`, `docs`, `interfaces`, `resources`, `tools`,
and `webmcp`, plus the root files. CI includes the checkout recipe.

```sh
WPT_ROOT=../wpt CHROME_BIN=/path/to/chrome-canary pnpm test:wpt
```

`WPT_PYTHON` and `WPT_VENV` optionally select the interpreter and environment.
On Ubuntu, CI installs an [AppArmor profile](https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md)
for Chrome to create its sandbox.

`wpt/run.ts` selects **every testharness test under `/webmcp`**, including
declarative and cross-document tests. Other WPT test types, such as crashtests,
are outside this lane. Native WebMCP is disabled and WPT injects the built
polyfill. Upstream test sources remain unchanged.

`wpt/metadata/` contains standard WPT expectations with a reason for each
affected file. Unexpected failures and unexpected passes fail the command.
File and subtest counts catch missing coverage and early harness exits.
Results and browser details are written to `wpt-results/report.json`.

### Recorded results

Chrome Canary 156.0.8062.0 reports **58 files and 139 subtests**:

| Subtest result | Count |
| ---------------- | ----: |
| PASS | 72 |
| Expected FAIL | 23 |
| Expected TIMEOUT | 25 |
| Expected NOTRUN | 19 |

At the file level, 32 harnesses finish with OK, 25 time out, and one reports an
expected setup error. All 22 IDL subtests pass. These results do not establish
full WebMCP conformance.

`NOTRUN` means an earlier subtest timed out before that subtest could run.
It is recorded separately from executed tests. The local browser suite covers
same-document cancellation and unregistration without lifecycle events.

### Why tests fail

- **Draft disagreements:** some pinned tests omit `executeTool()` input or expect
raw string results. The draft rejects non-object input, including omitted input,
and JSON-serializes callback results. Each affected subtest has an expectation.
- **Cross-document behavior:** tools and events stay in one document. Exposure,
frame-tree discovery, routing, and navigation cancellation are unsupported.
- **Declarative tools and lifecycle events:** form registration, CSS states, and
`toolactivated`/`toolcancel` events are not implemented. Tests waiting for
them time out; later subtests may not run.
- **Helper frames:** WPT injects into testharness pages, not initial `about:blank`
documents or `/common/blank.html`. Those frames have no polyfill. Local tests
install it in real served frames to exercise detachment.
- **Permissions Policy:** the polyfill checks the `tools` policy when the browser
exposes it. Otherwise, same-origin access approximates the default allowlist.
That fallback cannot honor an explicit denial or cross-origin permission.

## Draft alignment and limitations

The implementation was checked against
[draft source `df2d824`](https://github.com/webmachinelearning/webmcp/blob/df2d824e2cd2cbf8e15e25dad9dfe85d20e25082/index.bs).
Public declarations come directly from `webmcp-types@0.1.8`.

Timers approximate the WebMCP task source. Exact task ordering, navigation
cleanup, and native abort algorithms cannot be reproduced. An invocation aborted
before dispatch never starts its callback; the draft dispatches and then cancels
through the callback's signal.

Operations reject when `originAgentCluster` is false, except for `file:`
documents. Browsers without that property skip the check. Origin validation uses
URL parsing and scheme/host checks; browser-specific trusted schemes are not
recognized. Nonempty `exposedTo` and `fromOrigins` reject with
`NotSupportedError` after origin validation. Callbacks must validate their inputs.

When updating the draft or WPT pin, compare the
[draft history](https://github.com/webmachinelearning/webmcp/commits/main/index.bs),
[upstream tests](https://github.com/web-platform-tests/wpt/tree/master/webmcp), and
[types](https://github.com/webmachinelearning/webmcp-types).
Use [Blink source](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/core/script_tools/)
for Chromium-specific details. Review every changed expectation, update
`wpt/revision.txt` and the runner's coverage counts, and record browser versions
and results separately.
Loading