@josharsh/webmcp-bridge
v0.1.0
Published
Expose webmcp-tools tools as a real MCP server — lets browser extensions, iframe agents, and devtools list and call the page's WebMCP tools over postMessage.
Maintainers
Readme
@josharsh/webmcp-bridge
Expose the tools your page registered with webmcp-tools as a real MCP server, so external agents — browser extensions, iframe agents, devtools — can tools/list and tools/call them over window.postMessage.
The bridge mirrors the kit registry live: tool registrations and unregistrations are pushed to connected agents as notifications/tools/list_changed, and every call runs the tool's full pipeline (schema validation, confirm gate, result normalization).
Install
npm install webmcp-tools @josharsh/webmcp-bridgeIn the page (server side)
import { tool } from "webmcp-tools";
import {
createWebMCPServer,
PostMessageServerTransport,
} from "@josharsh/webmcp-bridge";
tool("add-to-cart", {
description: "Add a product to the shopping cart",
input: {
type: "object",
properties: { sku: { type: "string" } },
required: ["sku"],
},
run: ({ sku }) => cart.add(String(sku)),
});
const bridge = createWebMCPServer({ name: "my-shop", version: "1.0.0" });
await bridge.connect(
new PostMessageServerTransport({
// Only these origins may list/call your tools. Pass ["*"] explicitly
// (and knowingly) to accept any origin.
allowedOrigins: ["https://agent.example"],
}),
);In the agent (client side — extension content script, iframe, devtools)
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { PostMessageClientTransport } from "@josharsh/webmcp-bridge";
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(
new PostMessageClientTransport({
target: pageWindow, // e.g. an iframe's contentWindow, or window itself
targetOrigin: "https://shop.example",
}),
);
const { tools } = await client.listTools();
const result = await client.callTool({
name: "add-to-cart",
arguments: { sku: "SKU-1" },
});API
| Export | Description |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| createWebMCPServer(opts?) | Build an MCP server backed by the kit registry. opts: { name?, version? }. Returns { server, connect(transport), close() }. |
| WebMCPBridgeServer | Return type of createWebMCPServer. server is the raw SDK Server for custom handlers. |
| PostMessageServerTransport | new (opts: { window?, allowedOrigins, channel? }). Listens for client messages; rejects any origin not explicitly in allowedOrigins, binds exactly one peer (the {source, origin} of the first valid JSON-RPC message — later messages from other sources/origins are ignored), exposes the bound origin as peerOrigin, and replies to the bound event.source targeting its origin (never a wildcard when a concrete origin is known). |
| PostMessageClientTransport | new (opts: { target, targetOrigin, channel? }). Connects an SDK Client to a page's bridge; only consumes replies from targetOrigin. |
| DEFAULT_CHANNEL | "webmcp-tools-mcp" — the default envelope channel. Both peers must use the same channel. |
Semantics
exposedTofiltering. Tools registered withexposedTo: [origins]are only served (intools/listandtools/call) when the transport's boundpeerOriginis in the list. Tools withoutexposedToare served to every connected peer. Hidden tools are indistinguishable from unknown ones.- Unknown tools are JSON-RPC errors.
tools/callwith an unknown (or hidden) name responds with anInvalidParamsJSON-RPC error — SDK clients seeclient.callTool(...)reject. Tool execution failures (validation, declined confirm, thrownrun) still come back asisErrorresults. untrustedContentHintrides in_meta. The MCP SDK's annotations schema strips the WebMCP-specificuntrustedContentHint; the bridge preserves it astool._meta["webmcp/untrustedContentHint"](readOnlyHintstays inannotations).
Wire format
Messages are wrapped as { channel, side: "client" | "server", message: <JSON-RPC> } and everything else on the window is ignored. The side tag lets both peers share a single window (devtools, tests) without consuming their own traffic; the channel lets multiple bridges coexist.
License
MIT
