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
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ src/ ← core library (your focus)
├── context.tsx ← WebMCPProvider + useWebMCPStatus hook
├── hooks/
│ └── useMcpTool.ts ← main hook for tool registration
├── polyfill/ ← navigator.modelContext polyfill
├── polyfill/ ← document.modelContext polyfill
│ ├── index.ts ← installPolyfill / cleanupPolyfill + polyfill marker
│ ├── registry.ts ← in-memory tool storage
│ ├── testing-shim.ts ← simulates MCP client calls
Expand Down Expand Up @@ -64,7 +64,11 @@ These patterns look like they could be simplified but exist for specific reasons

**`"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 `navigator.modelContext` and skips installation if it exists. Don't remove this check — Chrome is shipping native WebMCP support.
**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.

**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.

## Testing

Expand Down
71 changes: 71 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Changelog

All notable changes to `webmcp-react` are documented here. The format is based on
[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).

## 0.2.0

Realigns the library with the current [WebMCP](https://github.com/webmachinelearning/webmcp)
spec. This is a clean break — the public surface (`<WebMCPProvider>`, `useMcpTool`,
`useWebMCPStatus`) is unchanged in shape, but the underlying WebMCP wiring has breaking
changes.

### Breaking changes

- **API moved to `document.modelContext`.** The registration API is now installed and detected
on `document.modelContext` instead of `navigator.modelContext`. Native detection is
document-only — `navigator.modelContext` is no longer read or written. (The testing/consumer
API stays on `navigator.modelContextTesting`.)
- **`registerTool` returns a `Promise`.** The polyfill's `registerTool(tool, options?)` now
returns a `Promise<undefined>` that rejects on:
- an empty/missing name, a name longer than 128 chars, or a name not matching
`^[A-Za-z0-9_.-]+$`;
- a duplicate name already registered;
- an empty/missing description;
- an `execute` that is not a function;
- a non-serializable `inputSchema`;
- an `exposedTo` entry that is not a parseable, potentially-trustworthy origin.

Already-aborted `AbortSignal` passed to `registerTool` resolves as a no-op (registration is
skipped), matching native Chrome 151. (Note: WebMCP spec PR #202 specifies rejection; native had
not shipped that as of Chrome 151. Revisit if native changes.)

`useMcpTool` routes these rejections into `state.error` and `onError`, except `AbortError`
(lifecycle teardown), which is ignored.
- **Unregistration is AbortSignal-only.** There is no `unregisterTool`. Tools are removed by
aborting the `AbortSignal` passed via `registerTool`'s options; `useMcpTool` does this on
unmount.
- **Single-argument handlers.** Tool `execute(input)` and user `handler(args)` now take a single
argument. `ModelContextClient` and `requestUserInteraction` are removed entirely.
- **`ToolAnnotations` narrowed** to `{ readOnlyHint?, untrustedContentHint? }`. The classic MCP
hints (`destructiveHint`, `idempotentHint`, `openWorldHint`) and `annotations.title` are
removed.
- **Top-level `title?` added** to the tool config and descriptor (replacing the dropped
`annotations.title`).
- **`exposedTo?: string[]` added** to the tool config and `RegisterToolOptions` for cross-frame
origin visibility. Changing `exposedTo` re-registers the tool.
- **`toolchange` event.** `document.modelContext` is an `EventTarget` that fires a bare
`toolchange` event (no `detail`) on tool register/unregister. `addEventListener("toolchange",
...)` and an `ontoolchange` handler are both supported.

### Retained library extensions

- **`outputSchema`** (Zod `output` / JSON-Schema `outputSchema`) is kept as a documented library
extension, not part of the spec descriptor.
- **`structuredContent`** on `CallToolResult` is retained.
- **Handlers always return a `CallToolResult`** with a `content` array (including error results
with `isError: true`) — a deliberate convention layered over the spec's looser return type so
results bridge cleanly to desktop MCP clients.

### Notes

- The **already-aborted-signal** behavior was confirmed against native Chrome 151: native skips
registration and resolves with `undefined` (a no-op) rather than rejecting, so the polyfill
matches. (WebMCP spec PR #202 specifies rejection; native had not shipped that as of Chrome 151.
Revisit if native changes.)

## 0.1.0

- Initial release: `<WebMCPProvider>`, `useMcpTool`, and `useWebMCPStatus` with a
`navigator.modelContext` polyfill, Zod and JSON-Schema tool definitions, and a testing shim.
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# webmcp-react

React hooks for exposing typed tools on `navigator.modelContext`.
React hooks for exposing typed tools on `document.modelContext`, aligned with the current [WebMCP](https://github.com/webmachinelearning/webmcp) spec.

[![npm version](https://img.shields.io/npm/v/webmcp-react)](https://www.npmjs.com/package/webmcp-react)
[![license](https://img.shields.io/npm/l/webmcp-react)](./LICENSE)
Expand Down Expand Up @@ -56,7 +56,7 @@ export default function App() {
}
```

That's it. The tool is registered on `navigator.modelContext` and can be called by WebMCP-compatible agents.
That's it. The tool is registered on `document.modelContext` and can be called by WebMCP-compatible agents.

### Using an AI agent?

Expand All @@ -70,15 +70,15 @@ Works with Cursor, Claude Code, GitHub Copilot, Cline, and [18+ other agents](ht

## How it works

[WebMCP](https://github.com/webmachinelearning/webmcp) is an emerging web standard that adds `navigator.modelContext` to the browser, an API that lets any page expose typed, callable tools to AI agents. Native browser support is still experimental and may evolve quickly. Chrome recently [released it in Early Preview](https://developer.chrome.com/blog/webmcp-epp).
[WebMCP](https://github.com/webmachinelearning/webmcp) is an emerging web standard that adds `document.modelContext` to the browser, an API that lets any page expose typed, callable tools to AI agents. Native browser support is still experimental and may evolve quickly. Chrome recently [released it in Early Preview](https://developer.chrome.com/blog/webmcp-epp).

This library provides React bindings for that API. `<WebMCPProvider>` installs a polyfill (skipped when native support exists), and each `useMcpTool` call registers a tool that agents can discover and execute.

![How webmcp-react works](./docs/architecture.svg)

## Connect to AI clients

Desktop MCP clients like Claude Code and Cursor can't access `navigator.modelContext` directly. The [WebMCP Bridge extension](https://chromewebstore.google.com/detail/webmcp-bridge/chgjbookknohehmaocfijekhaocaanaf) connects your registered tools to any MCP client.
Desktop MCP clients like Claude Code and Cursor can't access `document.modelContext` directly. The [WebMCP Bridge extension](https://chromewebstore.google.com/detail/webmcp-bridge/chgjbookknohehmaocfijekhaocaanaf) connects your registered tools to any MCP client.

1. Install the extension from the [Chrome Web Store](https://chromewebstore.google.com/detail/webmcp-bridge/chgjbookknohehmaocfijekhaocaanaf)
2. Configure your MCP client — see the [extension setup guide](./extension/README.md) for details
Expand Down Expand Up @@ -115,20 +115,20 @@ function TranslateTool() {
}
```

### Tool annotations
### Title and annotations

Hint AI agents about tool behavior with annotations (supports the [full MCP annotation set](https://modelcontextprotocol.io/docs/concepts/tools#annotations)):
Give a tool a human-friendly display `title`, and hint AI agents about its behavior with `annotations`. Per the current WebMCP spec, annotations are limited to `readOnlyHint` and `untrustedContentHint`:

```tsx
useMcpTool({
name: "delete_user",
description: "Permanently delete a user account",
input: z.object({ userId: z.string() }),
name: "search_users",
title: "Search users",
description: "Find users by name or email",
input: z.object({ query: z.string() }),
annotations: {
destructiveHint: true,
idempotentHint: true,
readOnlyHint: true,
},
handler: async ({ userId }) => { /* ... */ },
handler: async ({ query }) => { /* ... */ },
});
```

Expand Down Expand Up @@ -191,6 +191,20 @@ 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.

## 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:

- **API moved to `document.modelContext`.** Tools register on `document.modelContext` instead of `navigator.modelContext`. (The testing/consumer API stays on `navigator.modelContextTesting`.)
- **`registerTool` returns a `Promise`.** The polyfill's `registerTool(tool, options?)` returns a `Promise<undefined>` that rejects on invalid input (bad/duplicate/empty name, empty description, non-function execute, non-serializable `inputSchema`, untrustworthy `exposedTo` origin, or an already-aborted signal).
- **Unregistration is AbortSignal-only.** There is no `unregisterTool` — pass `{ signal }` and abort it. `useMcpTool` does this for you on unmount.
- **Handlers take a single argument.** `handler(args)` / `execute(input)` no longer receive a second `client` argument; `ModelContextClient` and `requestUserInteraction` are removed.
- **`annotations` narrowed to `{ readOnlyHint, untrustedContentHint }`.** The classic hints (`destructiveHint`, `idempotentHint`, `openWorldHint`) and `annotations.title` are removed. A new top-level `title?` field replaces `annotations.title`.
- **New `exposedTo?: string[]`** controls cross-frame origin visibility (changing it re-registers the tool).
- **New `toolchange` event.** `document.modelContext` is an `EventTarget` that fires a bare `toolchange` event on tool register/unregister; an `ontoolchange` handler is also supported.

`outputSchema` (Zod `output` / JSON-Schema `outputSchema`) and `structuredContent` on results are retained as documented library extensions.

## API

See the [full API reference](./docs/api.md).
Expand Down
Loading
Loading