Skip to content
Open
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: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ 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

### Changed

- **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.
Validation errors still take precedence over the aborted-signal check. `useMcpTool` is
unaffected: it never passes a pre-aborted signal, and `AbortError` rejections are already
treated as lifecycle teardown.
- 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).

## 0.2.0

Realigns the library with the current [WebMCP](https://github.com/webmachinelearning/webmcp)
Expand Down
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,6 @@ document.modelContext.ontoolchange = () => { /* ... */ };
- a non-serializable `inputSchema`;
- an `exposedTo` entry that is not a parseable, potentially-trustworthy origin.

An already-aborted `AbortSignal` does **not** reject: `registerTool` resolves as a no-op (registration is skipped), matching native Chrome 151. _(WebMCP spec PR #202 specifies rejection; native had not shipped that as of Chrome 151. Revisit if native changes.)_
An already-aborted `AbortSignal` also rejects: `registerTool` rejects with the signal's abort reason and skips registration, matching the WebMCP spec and native Chrome 152+. Validation errors take precedence — the aborted-signal check runs after the checks above.

The hook routes these rejections into `state.error` and fires `onError`, except `AbortError` (lifecycle teardown), which is ignored.
9 changes: 5 additions & 4 deletions examples/native-harness/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ async function runSelfTest(log: (line: string) => void) {
: `FAIL: add ${addRaw}`,
);

// Open spec question: does registerTool with an already-aborted signal
// reject (our current assumption) or resolve? Probe the live backend.
// Spec + native Chrome 152+: registerTool with an already-aborted signal
// rejects with the signal's abort reason. Older native (<=151) resolved as a
// no-op instead.
try {
const result = await mc.registerTool(
{
Expand All @@ -96,10 +97,10 @@ async function runSelfTest(log: (line: string) => void) {
},
{ signal: AbortSignal.abort(new DOMException("probe", "AbortError")) },
);
log(`INFO: already-aborted register RESOLVED (value=${String(result)})`);
log(`INFO: already-aborted register RESOLVED (value=${String(result)}; pre-152 native behavior)`);
} catch (err) {
const name = err instanceof Error ? err.name : String(err);
log(`INFO: already-aborted register REJECTED (${name})`);
log(`PASS: already-aborted register rejects (${name})`);
}
}

Expand Down
4 changes: 2 additions & 2 deletions extension/PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Privacy Policy — WebMCP Bridge

**Last updated:** March 2026
**Last updated:** July 2026

## Overview

WebMCP Bridge is an open-source Chrome extension that connects tools registered on web pages (via the `navigator.modelContext` browser API) to local AI assistants through Model Context Protocol (MCP). It does not collect, store, or transmit any personal data.
WebMCP Bridge is an open-source Chrome extension that connects tools registered on web pages (via the `document.modelContext` browser API) to local AI assistants through Model Context Protocol (MCP). It does not collect, store, or transmit any personal data.

## What data does the extension access?

Expand Down
6 changes: 3 additions & 3 deletions extension/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# WebMCP Bridge Server Chrome Extension

A Chrome extension that bridges tools registered on `navigator.modelContext` to desktop AI clients (Claude Code, Cursor, etc.) via MCP.
A Chrome extension that bridges tools registered on `document.modelContext` to desktop AI clients (Claude Code, Cursor, etc.) via MCP.

> Note: This extension is a workaround for now. Once Chrome natively supports some bridge solution, I'll deprecate this extension.

## How it works

Your React app registers tools using `webmcp-react`. The extension picks them up from `navigator.modelContext`, aggregates tools across all active tabs, and exposes them through a local MCP server that clients connect to over stdio.
Your React app registers tools using `webmcp-react`. The extension picks them up from `document.modelContext` (via the `navigator.modelContextTesting` consumer API), aggregates tools across all active tabs, and exposes them through a local MCP server that clients connect to over stdio.

![Extension architecture](./extension-architecture.svg)

Expand Down Expand Up @@ -97,7 +97,7 @@ After rebuilding, click the reload button on `chrome://extensions/` to pick up c
```
src/
├── background.ts # Service worker — manages tabs, tools, WebSocket
├── content-main.ts # Main world script — reads navigator.modelContext
├── content-main.ts # Main world script — reads navigator.modelContextTesting
├── content-isolated.ts # Isolated world script — bridges messages
├── types.ts # Shared type definitions
├── popup/
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"react",
"hooks",
"ai",
"navigator.modelContext",
"document.modelContext",
"w3c"
],
"author": "Kashish Hora",
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/useMcpTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,9 @@ export function useMcpTool(
};

try {
// Native Chrome (<=151) returns undefined and throws synchronously on error;
// the spec (#200) and our polyfill return a Promise<undefined> that rejects.
// Handle both shapes.
// Native Chrome <=151 returns undefined and throws synchronously on error;
// the spec, native Chrome 152+, and our polyfill return a Promise<undefined>
// that rejects. Handle both shapes.
const result: unknown = mc.registerTool(descriptor, {
signal: controller.signal,
...(cfg.exposedTo && { exposedTo: cfg.exposedTo }),
Expand Down
7 changes: 4 additions & 3 deletions src/polyfill/__tests__/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,12 @@ describe("createRegistry", () => {
).rejects.toMatchObject({ name: "SecurityError" });
});

it("resolves as a no-op when the signal is already aborted (matches native Chrome 151)", async () => {
it("rejects with the signal's abort reason when the signal is already aborted (matches native Chrome 152)", async () => {
const registry = createRegistry();
const reason = new DOMException("torn down", "AbortError");
await expect(
registry.registerTool(makeTool(), { signal: AbortSignal.abort() }),
).resolves.toBeUndefined();
registry.registerTool(makeTool(), { signal: AbortSignal.abort(reason) }),
).rejects.toBe(reason);
expect(registry.getTools().has("test_tool")).toBe(false);
});

Expand Down
6 changes: 3 additions & 3 deletions src/polyfill/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ export function createRegistry(): RegistryInternal {
}
}
if (options?.signal?.aborted) {
// Native Chrome (verified 151) resolves without registering when handed an
// already-aborted signal; match that rather than rejecting.
return Promise.resolve(undefined);
// Checked after validation — the spec gives validation errors precedence
// over an aborted signal, which rejects with its abort reason.
return Promise.reject(options.signal.reason);
}

tools.set(tool.name, {
Expand Down
Loading