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
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,33 @@ registration.abort();

For explicit installation, call `installWebMCP()` from `webmcp-polyfill`. Repeated calls are safe. Both entry points preserve existing native contexts, including partial implementations.

### Declarative tools

A form with `toolname` and `tooldescription` attributes is a tool. Its named controls make up the input schema, each described by its `toolparamdescription`, label, or `aria-description`. Disabled controls and controls with an applicable `readonly` attribute are left out. When multiple checkboxes or multiple radio buttons share a name, they form one parameter, described by the `toolparamdescription` of the nearest `fieldset` around them:

```html
<form toolname="search-flights" tooldescription="Search for flights" toolautosubmit>
<label>From <input name="from" required></label>
<label>To <input name="to" required></label>
<label>Date <input name="date" type="date"></label>
<button>Search</button>
</form>
```

A call fills the form, then submits it if the form has `toolautosubmit`. Without that attribute, the form needs an enabled submit button: the call focuses it and waits for the form's next submission. A submission the page does not cancel resolves the call with `null`, as does `form.submit()`; one it cancels without responding rejects the call. To respond, call `preventDefault()` and then `respondWith()` in the `submit` listener:

```js
const form = document.querySelector('form[toolname="search-flights"]');
form.addEventListener("submit", (event) => {
if (event.agentInvoked) {
event.preventDefault();
event.respondWith(searchFlights(new FormData(form)));
}
});
```

The call resolves with the response: objects are JSON-serialized, and other values are converted to strings. A rejected response or a form reset rejects the call. So does removing the form or changing its tool attributes, unless the page does that during the agent's `submit` event.

### Script tag

Serve the built `dist/polyfill.js` before your app:
Expand Down Expand Up @@ -89,22 +116,18 @@ For cross-origin tools, delegate the `tools` permission on the iframe, register
<iframe src="https://tools.example/app" allow="tools https://tools.example"></iframe>
```

Initial discovery waits up to 500 ms for existing frames. Requests use `MessageChannel` after checking the peer's source and origin; callbacks run in their owning frame. Cancellation preserves the caller's reason and sends the callback a default `AbortError`.
Initial discovery waits up to 500 ms for existing frames. Callbacks run in their owning frame. Cancellation preserves the caller's reason and sends the callback a default `AbortError`.

## Implementation status

The target is `webmcp-types@0.1.10`: registration, discovery, execution, cancellation, `toolchange`, and the `toolactivated`/`toolcancel` lifecycle events, including frame exposure and origin filtering. Declarative forms are not implemented. Browser agent integration requires browser support.
The target is `webmcp-types`: registration, discovery, execution, cancellation, `toolchange`, and the `toolactivated`/`toolcancel` lifecycle events, including frame exposure and origin filtering. The polyfill also implements declarative tools, which follow the [declarative API explainer](https://github.com/webmachinelearning/webmcp/blob/main/declarative-api-explainer.md) and Chromium because the draft leaves them unwritten. The `:tool-form-active` and `:tool-submit-active` pseudo-classes are not implemented. Browser agent integration requires browser support.

Native contexts do not join the polyfill's channels. See [TESTING.md](https://github.com/webmachinelearning/webmcp-polyfill/blob/main/TESTING.md) for policy and frame limitations, results, commands, and tracked revisions.

`executeTool()` accepts an object and returns a JSON-serialized result. Omitted or `undefined` input defaults to a fresh empty object. Callbacks must validate their inputs; schema inference provides TypeScript checks only.
`executeTool()` accepts an object and returns a JSON-serialized result, or a declarative tool's response, which is `null` if its form navigates. Omitted or `undefined` input defaults to a fresh empty object. Callbacks must validate their inputs; schema inference provides TypeScript checks only.

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

## Development

`src/` holds the polyfill, `tests/` the browser and package checks, and `wpt/` the upstream runner, pin, and expectations.

## License

[MIT](LICENSE).
99 changes: 83 additions & 16 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Results

**238 browser tests pass** across Chromium 153.0.8010.12, Firefox 155.0, and
**352 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.
Expand All @@ -12,19 +12,20 @@ in Chrome and Firefox:

| Subtest result | Count |
| --- | ---: |
| PASS | 116 |
| Expected FAIL | 33 |
| Expected TIMEOUT | 25 |
| Expected NOTRUN | 16 |
| PASS | 147 |
| Expected FAIL | 37 |
| Expected TIMEOUT | 5 |
| Expected NOTRUN | 1 |

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: 44 OK, 25 expected timeouts, one expected error.
Tested locally with Chrome Canary 157.0.8084.0 and Firefox Nightly 158.0a1.
At the file level: 63 OK, five expected timeouts, two expected errors.
Safari may report the detached-frame test before its unhandled rejection reaches
the harness, so that file allows either OK or ERROR; its subtest must still FAIL.

Expected failures are still failures. `NOTRUN` means an earlier timeout prevented
the test from running, including one abort case. Passing declarative checks only
cover rejection or absence of tools. All 38 pinned IDL checks pass. This is not
full conformance.
the test from running, including one abort case. All 38 pinned IDL checks pass;
the pinned IDL does not include the declarative `SubmitEvent` members. This is
not full conformance.

## Run locally

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

Use WPT for shared conformance cases. Local tests cover installation, documented
polyfill differences, and assertions that WPT does not make or reach. Compare
individual assertions and recorded results before adding coverage: the pinned
manual-submit, reset, and abort tests stop at unsupported pseudo-class checks,
so local tests still exercise the behavior beyond those failures.

WPT needs Python 3.11+ and a clean checkout at
[`fe52996`](https://github.com/web-platform-tests/wpt/commit/fe52996d4465f23617bce91927bdd58e6ce8f541).
The [CI workflow](.github/workflows/test.yml) has the sparse-checkout and dependency setup.
Expand All @@ -53,6 +60,11 @@ WPT_ROOT=../wpt WPT_BROWSER=safari pnpm test:wpt

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).
Safari 26.6.2 opens a native confirmation sheet when the HTTPS tests submit to
`about:blank`. It prevents WebDriver from closing four test tabs, so WPT discards
five subtest results and fails the completeness check at 185/190. The current
warning does not honor `AskBeforeSubmittingInsecureForms`. Using an HTTPS form
destination avoids the warning; that fixture repair needs to land in WPT.

Set `WPT_PYTHON` or `WPT_VENV` to use an existing Python environment. Extra arguments
go to WPT. On macOS, Firefox may need `--certutil-binary` pointing to a wrapper that
Expand All @@ -68,13 +80,66 @@ other non-testharness files are outside this suite.
Checked against [draft `d61d0e6`](https://github.com/webmachinelearning/webmcp/blob/d61d0e6d297ddb6bff3510b1330dbb215c6ef43c/index.bs)
and `webmcp-types@0.1.10`.

- **Missing APIs:** declarative forms and CSS states are not implemented.
- **Draft differences:** results are JSON-serialized; some pinned tests expect raw
strings. Omitted or `undefined` input becomes `{}`; `null` and primitives reject.
- **Missing APIs:** scripts cannot add selectors, so the `:tool-form-active` and
`:tool-submit-active` pseudo-classes are unsupported.
- **Pinned WPT differences:** callback results are JSON-serialized as the draft
requires; some pinned tests expect raw strings.
- **Lifecycle events:** `toolactivated` fires before the callback is invoked, as the
draft specifies; the pinned `executeTool-abort` test and Chromium fire it after the
callback starts. Script-dispatched events cannot be
[trusted](https://dom.spec.whatwg.org/#dom-event-istrusted), so `isTrusted` is false.
- **Declarative tools:** the draft's declarative section is a TODO, so they follow the
[explainer](https://github.com/webmachinelearning/webmcp/blob/d61d0e6d297ddb6bff3510b1330dbb215c6ef43c/declarative-api-explainer.md)
and Chromium at [`dbdbb13`](https://chromium.googlesource.com/chromium/src/+/dbdbb13fd74c9411ca2e39ba087fa2e031184ff5/third_party/blink/renderer/core/html/forms/).
Schemas match the cases in Chromium's `html_form_mcp_tool_test.cc` at that revision,
except those behind its file-input and custom-element flags; those controls are
unsupported. As in Chromium and the pinned tests, a submission that navigates
resolves `executeTool()` with `null`, although the draft and `webmcp-types` declare a
string. Installation adds `agentInvoked` and `respondWith()` to
`SubmitEvent.prototype` and wraps `HTMLFormElement.prototype.submit()`. Differences
from Chromium:
- `toolactivated` fires once the form is filled, before it submits or waits for
the user, as the explainer describes; Chromium fires it afterwards, even when
filling fails.
- From the agent's submit event until the polyfill settles the submission in a
later task, a removal or tool attribute change keeps the call, and
`respondWith()` is accepted; Chromium allows both only during the event's
dispatch, which includes microtasks that listeners queue.
- With `toolautosubmit`, the polyfill submits from script, so those microtasks run
after the dispatch, and `preventDefault()` after an `await` no longer stops the
submission; Chromium submits natively and honors it.
- A change to the form's controls replaces the tool without cancelling a call that
waits for the user; Chromium cancels it.
- Moving a form that waits for the user, which mutation observers see as no
change, keeps its call; Chromium cancels it.
- A reset cancels a call only if it reaches the polyfill's window listener
uncanceled. Chromium also cancels the call when a listener stops the reset's
propagation, and keeps it when a later window listener cancels the reset.
- A newer call rejects an older one that waits for the user, while one that
waits for the page's response still settles; Chromium leaves the older call
pending.
- Only a call's first submission is the agent's; Chromium also counts a later
submission while the page's response is pending.
- A window capture listener that the page added before installation can stop the
agent's `submit` event before the polyfill sees it, unless the listener reads
`agentInvoked` or calls `respondWith()` first.
- When a name frees up, the first form in document order that claims it
registers; Chromium registers a form whose name was taken only when that form
changes.
- A submission that fails validation keeps a call that waits for the user;
Chromium rejects it.
- Numbers fill controls as `String()` converts them; Chromium formats those that
are not 32-bit integers with six significant digits, and rejects them for
checkboxes.
- Numeric schema values use JavaScript numbers. Step-base divisibility uses the
raw decimal attributes, up to 18 coefficient digits and exponents from -1023
to 1023. Beyond those bounds, `multipleOf` is omitted instead of reproducing
Blink's Decimal rounding. Its conversion to a schema number can also round
differently from JavaScript.
- The fill's `input` and `change` events are untrusted.
- `SubmitEventInit` has no `agentInvoked` member, as in the explainer.
- Forms in shadow trees are unsupported; Chromium registers them, including in
closed shadow roots.
- **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
Expand All @@ -92,8 +157,10 @@ and `webmcp-types@0.1.10`.
excludes it.
- **Policy and origins:** without native policy introspection, only accessible
iframe delegation can be checked, not HTTP Permissions Policy. Cross-origin
ancestors must also load the polyfill. Navigation inheritance is approximate;
browser-specific trusted schemes and opaque execution origins are unsupported.
ancestors must also load the polyfill; if they load it after a frame's startup
wait, that frame's forms register at its next API call. Navigation inheritance is
approximate; browser-specific trusted schemes and opaque execution origins are
unsupported.

When updating the pins, compare the [draft](https://webmachinelearning.github.io/webmcp/),
[WPT](https://github.com/web-platform-tests/wpt/tree/master/webmcp), and
Expand Down
Loading
Loading