Skip to content
Closed
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
18 changes: 12 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ src/ ← core library (your focus)
├── polyfill/ ← document.modelContext polyfill
│ ├── index.ts ← installPolyfill / cleanupPolyfill + polyfill marker
│ ├── registry.ts ← in-memory tool storage
│ ├── testing-shim.ts ← simulates MCP client calls
│ ├── execute.ts ← shared execution engine (signals, serialization, errors)
│ ├── testing-shim.ts ← DEPRECATED wrapper over execute.ts (removed in 0.4.0)
│ └── validation.ts ← input validation against JSON Schema
└── utils/
├── schema.ts ← Zod → JSON Schema conversion + schema fingerprinting
Expand All @@ -58,27 +59,32 @@ These patterns look like they could be simplified but exist for specific reasons

**Schema fingerprinting** (`schemaFingerprint` in `utils/schema.ts`, used by `useMcpTool.ts`): useEffect deps use string fingerprints of schemas, not object references, to prevent infinite re-registration loops when schema objects are recreated each render. Don't switch to direct object comparison.

**Dual execution paths**: Tools execute via `execute()` (internal/UI calls) and via the testing shim (external MCP client calls). Both paths update the same reactive state and fire the same callbacks. Changes to one path must be mirrored in the other.
**Dual execution paths**: Tools execute via `execute()` (internal/UI calls) and via `document.modelContext.executeTool()` (external MCP client calls, native or polyfilled). Both paths update the same reactive state and fire the same callbacks. Changes to one path must be mirrored in the other.

**Ref-wrapped config** (`configRef`, `handlerRef`, etc.): Refs wrap mutable config so the registration useEffect doesn't re-run on every render. These are not missed dependencies — they're intentional stability optimizations.

**`"use client"` banner**: Added at build time via `tsup.config.ts`, not in source files. This makes the library work with Next.js SSR. Don't add `"use client"` to source files.

**Native API detection**: The polyfill checks for native `document.modelContext` (document-only — it does not read `navigator.modelContext`) and skips installation if it exists. Don't remove this check — Chrome is shipping native WebMCP support.

**AbortSignal-only unregistration**: There is no `unregisterTool`. Tools are removed by aborting the `AbortSignal` passed to `registerTool`. `useMcpTool` aborts its controller on cleanup; the registry's abort listener removes the tool. Don't reintroduce an imperative unregister method.
**AbortSignal-only unregistration**: There is no `unregisterTool`. Tools are removed by aborting the `AbortSignal` passed to `registerTool`. `useMcpTool` aborts its controller on cleanup; the registry's abort listener removes the tool. Don't reintroduce an imperative unregister method. Unregistration does not cancel in-flight executions (Chrome 153.0.8008.0+); they run to completion.

**Single-arg execute/handler**: `descriptor.execute(input)` and the user `handler(args)` take a single argument. There is no `ModelContextClient` second argument. Both execution paths must stay mirrored.
**Two-arg execute/handler**: `descriptor.execute(input, { signal })` and the user
`handler(args, ctx)` receive the execution `AbortSignal` as their second argument (Chrome
153+ shape). There is still no `ModelContextClient`. A missing second argument at runtime
(Chrome ≤152) is substituted with a never-aborting signal. Both execution paths must stay
mirrored — they share `runHandler` in `useMcpTool.ts` and `runTool` in
`polyfill/execute.ts`.

## Testing

- **Framework**: Vitest + React Testing Library + jsdom
- **Location**: `__tests__/` directories adjacent to source files
- **StrictMode**: All tests must pass under React StrictMode (double-mount behavior)
- **Testing shim**: `polyfill/testing-shim.ts` simulates external MCP client calls — use it in tests to verify the full registration → execution → state update cycle
- **External-call path**: `document.modelContext.getTools()` / `executeTool()` is the consumer API — use it in tests to verify the full registration → execution → state update cycle. `navigator.modelContextTesting` (`polyfill/testing-shim.ts`) delegates to the same engine but is deprecated and is removed in 0.4.0; don't write new tests against it
- **Coverage areas**: Registration lifecycle, execution state, error handling, input validation, SSR safety, StrictMode compatibility

When adding features, write tests that cover both execution paths (direct `execute()` and testing shim `executeTool()`).
When adding features, write tests that cover both execution paths (direct `execute()` and `document.modelContext.executeTool()`).

## Code Style

Expand Down
37 changes: 36 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,39 @@ All notable changes to `webmcp-react` are documented here. The format is based o
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
## 0.3.0

Tracks Chrome 152–154 WebMCP changes: execution AbortSignals, the
`document.modelContext` consumer API, and the removal of
`navigator.modelContextTesting` from native Chrome.

### Added

- **Handlers receive an execution `AbortSignal`.** Handlers are now called as
`handler(args, { signal })`; on Chrome 153.0.8007.0+ the signal aborts when the agent or
user cancels the call — pass it to `fetch()` and other cancellable work. On Chrome ≤152
(and for bare `execute(args)` calls) the library substitutes a never-aborting signal, so
the second argument is always safe to use. The hook's `execute(input?, { signal }?)`
accepts a caller signal too. Existing one-argument handlers keep working unchanged.
- **Polyfill consumer API.** `document.modelContext.getTools()` and
`executeTool(tool, inputArguments, { signal }?)`, matching native Chrome:
`RegisteredTool.inputSchema` is a deep-copied object (Chrome 154.0.8014.0+ shape),
`inputArguments` may be a JSON string or an object, execution failures reject with
`UnknownError`, aborts reject with the signal's reason, and unregistering a tool no
longer cancels in-flight executions (Chrome 153.0.8008.0+ behavior). The polyfill
additionally validates input against `inputSchema` (`OperationError`) — native Chrome
does not validate yet.
- New exported types: `ToolExecuteCallbackOptions`, `ExecuteToolOptions`,
`RegisteredTool`, `ModelContextGetToolOptions`.

### Changed

- **Abort is cancellation, not error.** When an execution's signal aborts and the handler
rejects, the hook clears `isExecuting` but leaves `state.error` untouched and does not
fire `onError`.
- The testing shim's abort rejections now use the signal's abort reason and its tool
failures reject with `UnknownError` (Chrome 152+ parity); its `OperationError` input
errors and `NotFoundError` are unchanged.
- **Already-aborted `AbortSignal` now rejects.** The polyfill's `registerTool` rejects with the
signal's abort reason when handed an already-aborted signal, matching WebMCP spec PR #202 and
native Chrome 152.0.7943.0. Previously it resolved as a no-op, matching native Chrome 151.
Expand All @@ -17,6 +46,12 @@ All notable changes to `webmcp-react` are documented here. The format is based o
- Docs and npm keywords now reference `document.modelContext`; `navigator.modelContext` was
removed from Chrome as of 152.0.7943.0 (the library itself migrated in 0.2.0).

### Deprecated

- **`navigator.modelContextTesting`.** Native Chrome removed it in 152.0.7940.0. The
polyfill's shim now delegates to the same engine as `document.modelContext.executeTool()`
and warns once in dev. It will be removed in webmcp-react 0.4.0.

## 0.2.0

Realigns the library with the current [WebMCP](https://github.com/webmachinelearning/webmcp)
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,17 @@ useMcpTool({

Works with Next.js, Remix, and any server-rendering framework out of the box. The build includes a `"use client"` banner, so no extra configuration is needed.

## What's new in 0.3.0

- **Handlers get an execution `AbortSignal`**: `handler(args, { signal })`. Chrome 153+
aborts it when the agent cancels the call; on older Chrome the library substitutes a
never-aborting signal, so `fetch(url, { signal })` is always safe. One-argument handlers
keep working.
- **Consumer API in the polyfill**: `document.modelContext.getTools()` /
`executeTool(tool, args, { signal }?)` — the same surface native Chrome 152+ ships.
- **`navigator.modelContextTesting` is deprecated** (removed from native Chrome in 152;
removed from this library in 0.4.0).

## Breaking changes in 0.2.0

0.2.0 realigns the library with the current [WebMCP](https://github.com/webmachinelearning/webmcp) spec. If you're upgrading from 0.1.0:
Expand Down
32 changes: 27 additions & 5 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# API Reference

This library targets the current [WebMCP](https://github.com/webmachinelearning/webmcp) spec. The registration API lives on `document.modelContext` (an `EventTarget`), and the testing/consumer API lives on `navigator.modelContextTesting`.
This library targets the current [WebMCP](https://github.com/webmachinelearning/webmcp) spec. Both the registration and consumer APIs live on `document.modelContext` (an `EventTarget`): `registerTool` on the registration side, `getTools()`/`executeTool()` on the consumer side. `navigator.modelContextTesting` is a deprecated wrapper over the same consumer engine.

## `<WebMCPProvider>`

Expand Down Expand Up @@ -43,11 +43,11 @@ Registers a tool on `document.modelContext`. Automatically unregisters on unmoun
| `output` | `z.ZodObject` | Optional Zod schema for outputs (library extension; see below) |
| `annotations` | `ToolAnnotations` | Optional behavior hints (`readOnlyHint`, `untrustedContentHint`) |
| `exposedTo` | `string[]` | Optional list of trustworthy origins this tool is exposed to across frames |
| `handler` | `(args) => CallToolResult \| Promise<CallToolResult>` | Tool implementation. Receives a single argument (the parsed input) |
| `handler` | `(args, ctx) => CallToolResult \| Promise<CallToolResult>` | Tool implementation. Receives the parsed input and `ctx: { signal: AbortSignal }`; the signal aborts when the agent cancels the execution (Chrome 153+; otherwise a never-aborting substitute) |
| `onSuccess` | `(result) => void` | Optional callback on success |
| `onError` | `(error) => void` | Optional callback on error |

The `handler` takes a **single argument** — the validated input object. There is no second `client` argument.
The `handler` receives the validated input object and a second `ctx` argument containing the execution `AbortSignal`. Handlers that declare a single parameter keep working.

### JSON Schema config

Expand Down Expand Up @@ -80,11 +80,20 @@ const { state, execute, reset } = useMcpTool({ ... });
| `state.lastResult` | `CallToolResult \| null` | Most recent result |
| `state.error` | `Error \| null` | Most recent error |
| `state.executionCount` | `number` | Total successful executions |
| `execute(input?)` | `(input?) => Promise<CallToolResult>` | Manually invoke the tool |
| `execute(input?, { signal }?)` | `(input?, options?) => Promise<CallToolResult>` | Manually invoke the tool |
| `reset()` | `() => void` | Reset state to initial values |

`execute()` (the UI/direct path) throws if validation or handler logic fails. The agent/testing-shim path returns a `CallToolResult` with `isError: true` instead. Both paths update the same reactive state and fire the same `onSuccess`/`onError` callbacks.

### Cancellation

Each execution gets its own `AbortSignal`, passed to the handler as `ctx.signal`. On
Chrome 153.0.8007.0+ (and via the polyfill's `executeTool`) it aborts when the caller
cancels. When an aborted execution's handler rejects, the hook treats it as
**cancellation**: `isExecuting` clears, but `state.error` stays untouched and `onError`
does not fire. Unregistering a tool (unmount) does **not** cancel in-flight executions
(Chrome 153.0.8008.0+ behavior).

## Results: `CallToolResult`

Handlers always return a `CallToolResult` with a `content` array — including error results, which set `isError: true`. This is a deliberate library convention layered over the spec's looser return type, so results bridge cleanly to desktop MCP clients.
Expand All @@ -104,7 +113,20 @@ interface CallToolResult {
When native WebMCP is unavailable, the provider installs a polyfill that exposes:

- `document.modelContext` — the registration API (an `EventTarget`). `registerTool(tool, options?)` returns a `Promise<undefined>` that **rejects** on invalid input (see below). Unregistration is **AbortSignal-only** — pass `{ signal }` and abort it to remove the tool. There is no `unregisterTool`.
- `navigator.modelContextTesting` — the consumer/testing API (`listTools()`, `executeTool(name, argsJson, options?)`, `registerToolsChangedCallback(cb)`, `getCrossDocumentScriptToolResult()`). Browser extensions and tests use this to discover and invoke tools.
- `document.modelContext.getTools(options?)` / `executeTool(tool, inputArguments, options?)`
— the consumer API (same shape as native Chrome). `getTools()` resolves sorted, fresh
`RegisteredTool` objects whose `inputSchema` is a deep-copied **object**;
`executeTool` accepts a JSON string or object input, forwards `options.signal` into the
tool's execution signal, rejects `UnknownError` on failure, and — unlike native Chrome —
validates input against `inputSchema` (`OperationError`).
- `navigator.modelContextTesting` — **deprecated** wrapper over the same engine
(`listTools()` keeps returning a JSON-string `inputSchema`); removed in 0.4.0.

| Chrome | Behavior this library tracks |
| --- | --- |
| ≤152 | `execute(input)` — no tool-side signal (the library substitutes one); `navigator.modelContextTesting` removed in 152.0.7940.0 |
| 153 | `execute(input, { signal })`; unregistration no longer cancels in-flight executions (153.0.8008.0+) |
| 154 | `RegisteredTool.inputSchema` is an object (was a JSON string) |

The native API is detected by reading `document.modelContext` only; the polyfill marks itself with `__isWebMCPPolyfill` so native support short-circuits installation.

Expand Down
Loading
Loading