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
13 changes: 12 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,18 @@ jobs:
- run: npm install --global pnpm@10.14.0
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps chromium firefox webkit
- uses: browser-actions/setup-firefox@0bc507ddf224827e3b1af68e014d5e42ab93e795 # v1.7.2
id: firefox
with:
firefox-version: latest
# Pinned to the tip of this action's `latest` branch; it publishes no release tags.
- uses: browser-actions/setup-geckodriver@fac5b0424c584257f32cf12c5eb4ba86014c34f8
with:
geckodriver-version: 0.37.1
token: ${{ github.token }}
- run: pnpm test
env:
FIREFOX_BIN: ${{ steps.firefox.outputs.firefox-path }}
- run: pnpm test:package
- uses: actions/upload-artifact@v7
if: always()
Expand Down Expand Up @@ -63,7 +74,7 @@ jobs:
python-version: "3.11"
- run: npm install --global pnpm@10.14.0
- run: pnpm install --frozen-lockfile
- uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd
- uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2
id: chrome
with:
chrome-version: canary
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ compatibility belongs here.
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.
extension APIs, page requests, or tool callbacks with mocks.

## Validate

Expand Down
80 changes: 80 additions & 0 deletions EXTENSIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Add WebMCP to an existing extension

Install the polyfill in the page's **MAIN** world. An isolated content script has its own JavaScript globals and cannot install or read the page's `document.modelContext`. Keep extension APIs, credentials, and permission decisions in the extension's isolated context.

## Install at document start

Copy the built `dist/polyfill.js` into your extension as `webmcp-polyfill.js`. It is minified; `dist/index.js` is the same implementation unminified if you need to read it. Add this entry to your existing manifest, replacing the match pattern with the origins your extension supports:

```json
{
"content_scripts": [
{
"matches": ["https://example.com/*"],
"js": ["webmcp-polyfill.js"],
"world": "MAIN",
"run_at": "document_start"
}
]
}
```

The bundle is self-contained. It needs no web-accessible resources, network requests, MCP server, or extension-specific runtime. Keep top-frame-only injection for the initial document-local implementation.

Use host match patterns without a port; enforce an exact origin separately where your extension needs that restriction.

The page still needs the browser prerequisites described in the README, including an origin-keyed agent cluster. Injecting a script cannot supply a missing `Origin-Agent-Cluster` response header or bypass the page's Permissions Policy.

## Discover and execute from the extension

A Chromium extension service worker or Firefox background script can use `chrome.scripting.executeScript` in the MAIN world. The browser carries the result back; there is no need to create a page-message request protocol. The extension needs the `scripting` permission and access to the target page through a host permission or `activeTab`.

```js
const [{ result: tools }] = await chrome.scripting.executeScript({
target: { tabId },
world: "MAIN",
func: async () => {
const tools = await document.modelContext.getTools();
// WindowProxy cannot cross the extension serialization boundary.
return tools.map(({ window, ...metadata }) => metadata);
},
});

const [{ result }] = await chrome.scripting.executeScript({
target: { tabId },
world: "MAIN",
args: [toolName, inputObject],
func: async (name, input) => {
const context = document.modelContext;
const tool = (await context.getTools()).find((tool) => tool.name === name);
if (!tool) throw new Error("Tool is no longer available");
return context.executeTool(tool, input);
},
});
```

`tabId`, `toolName`, and `inputObject` come from your extension's own UI or agent. Match the target document as well as the tab when retaining a selection across navigation. Re-check availability and permissions before acting. This example assumes the object-input execution API; older native Chrome implementations are outside this package's compatibility scope.

`AbortSignal` cannot cross `executeScript` arguments, so the example above has no cancellation. The polyfill supports execution signals; to use them, create the `AbortController` inside the MAIN-world function and drive it from your extension's own cancellation path.

## Observe changes with DOM events

For live discovery, a MAIN-world content script can listen to the existing `toolchange` event. If your isolated content script needs a notification, forward a payload-free DOM event on the shared document:

```js
// MAIN world: load after webmcp-polyfill.js.
document.modelContext.addEventListener("toolchange", () => {
document.dispatchEvent(new Event("my-extension:webmcp-tools-changed"));
});

// ISOLATED world: use your existing refresh and extension messaging code.
document.addEventListener("my-extension:webmcp-tools-changed", () => {
// Ask the extension to refresh its tool list using the discovery call above.
});
```

Fetch the initial list after your listener is installed. Notifications are hints: page scripts can forge or suppress them. They must never authorize tool execution or privileged extension actions. Treat discovered metadata and results as page-controlled data too.

Chrome documents [execution worlds and content-script injection](https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts) and [the scripting API](https://developer.chrome.com/docs/extensions/reference/api/scripting). MAIN-world code is visible to and affected by the page; DOM events are communication, not an authentication boundary.

The integration tests run these examples in both Chromium and Firefox. Firefox also exposes the Promise-based `browser.scripting` namespace. See [Mozilla's scripting API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript) for browser-specific result and error handling.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ The implementation follows [draft source `cc45efc`](https://github.com/webmachin

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

For use in an existing extension, see [extension integration](EXTENSIONS.md).

## Development

See [TESTING.md](https://github.com/webmachinelearning/webmcp-polyfill/blob/main/TESTING.md) for browser setup, test commands, draft alignment, and known limitations.
Expand Down
42 changes: 29 additions & 13 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,41 @@ pnpm test
pnpm test:package
```

| Check | What it runs |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `index.test-d.ts` | TypeScript against the built package declarations, including upstream schema inference |
| `index.test.ts` | Built bundle served over HTTP in Chromium (native WebMCP disabled), Firefox, and WebKit; coercion, metadata, events, errors, detached documents |
| `execute.test.ts` | The same three engines: object input, JSON results, cancellation, concurrent calls, and dispatch-time failures |
| `app.test.ts` | A served application, real button interactions, callback side effects, invalid input, unregistration, and reload |
| `native.test.ts` | Real native Chromium registration, then polyfill loading; context and getter identities must survive |
| `pnpm test:package` | Packed tarball installed into a fresh consumer, public type imports, SSR-safe entry points, and package contents |
| `pnpm test:wpt` | Unmodified upstream WPT and IDL in real Chrome Canary, with native WebMCP disabled |
The Firefox extension project additionally requires a stock Firefox and
[geckodriver](https://github.com/mozilla/geckodriver/releases). Put geckodriver
on PATH, or set `GECKODRIVER` to its executable. Set `FIREFOX_BIN` when Firefox
is not discoverable by geckodriver. CI installs both explicitly. No extension
signing preference is disabled: geckodriver installs a temporary development
add-on into a disposable profile.

For a focused run, use `pnpm build && pnpm exec playwright test --project=extension-firefox`
or `--project=extension-chromium`; every browser test loads the built bundle, so skipping the
build tests the previous one. Missing prerequisites fail that project.

| Check | What it runs |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `index.test-d.ts` | TypeScript against the built package declarations, including upstream schema inference |
| `index.test.ts` | Built bundle served over HTTP in Chromium (native WebMCP disabled), Firefox, and WebKit; coercion, metadata, events, errors, detached documents |
| `execute.test.ts` | The same three engines: object input, JSON results, cancellation, concurrent calls, and dispatch-time failures |
| `app.test.ts` | A served application, real button interactions, callback side effects, invalid input, unregistration, and reload |
| `native.test.ts` | Real native Chromium registration, then polyfill loading; context and getter identities must survive |
| `extension.test.ts` | Real MV3 extension in Chromium and stock Firefox: MAIN installation, isolated notifications, background scripting calls, page state, re-registration, reload, and an unmatched origin |
| `pnpm test:package` | Packed tarball installed into a fresh consumer, public type imports, SSR-safe entry points, and package contents |
| `pnpm test:wpt` | Unmodified upstream WPT and IDL in real Chrome Canary, with native WebMCP disabled |

The fixture server binds 127.0.0.1:8793 and sets the required `Origin-Agent-Cluster`
header; Playwright never reuses an existing server, so free that port first.
Loopback HTTP is a secure context;
Geckodriver picks its own loopback port. Loopback HTTP is a secure context;
the WPT `non-secure.html` case supplies the nonsecure-origin check. The browser
suite uses fresh contexts and real network responses, without route interception,
fake timers, DOM shims, or mocked tool callbacks.
fake timers, DOM shims, or mocked extension APIs. The extension fixture executes
the integration guide's discovery/invocation example directly.

Playwright retains failure traces and an HTML report in `playwright-report/`.
Playwright drives its bundled Firefox for page tests. WebKit is additional engine
coverage, not a claim that Safari itself was tested.
Firefox extension runs attach the browser version, a page screenshot, and
geckodriver logs. Page tests use Playwright's bundled Firefox. Extension tests use stock Firefox
over Selenium, because Playwright loads extensions only in Chromium. WebKit is
engine coverage, not a claim that Safari was tested.

## Run upstream WPT

Expand Down Expand Up @@ -115,6 +130,7 @@ it, and checks all three operations and exception realms.
| [Official types](https://github.com/webmachinelearning/webmcp-types) | Public declarations, schema inference, and pending API updates |
| [Blink script_tools](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/core/script_tools/) | Chromium IDL, implementation, tests, and commit-linked bugs |
| [Gecko source search](https://searchfox.org/mozilla-central/search?q=ModelContext) and [Mozilla position](https://github.com/mozilla/standards-positions/issues/1412) | Locate Firefox implementation work and discussion; a position is not evidence of shipped support |
| [Firefox extension worlds](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_scripts) and [scripting](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript) | Browser extension integration and serialization boundaries |

Reproduce a disagreement before changing code or expectations, and record what
changed in the draft, types, WPT, and browser implementation separately.
Expand Down
Loading