Skip to main content
@mcp-b/webmcp-local-relay connects WebMCP tools running in browser tabs to desktop MCP clients (Claude Desktop, Cursor, Claude Code, Windsurf) via a localhost WebSocket relay and stdio transport.
Package metadata

Architecture

The relay has four layers: For a conceptual discussion of transports and bridges, see Transports and Bridges.

Installation

Add this JSON block to your MCP client configuration:
You can also run the relay directly:
A .mcpb bundle file is available from GitHub Releases for Claude Desktop (double-click to install). It starts with the zero-configuration defaults: loopback, automatic port discovery, and all page origins allowed.

CLI options

Static management tools

The relay always exposes three management tools:

Dynamic tool registration

Tools registered on webpages are forwarded to the MCP server as first-class tools. Tool names are sanitized to the character set [a-zA-Z0-9_]. When multiple tabs register a tool with the same name, a short tab-ID suffix disambiguates them: Tools appear and disappear as tabs open, reload, and close. Names are limited to 128 characters. Sanitization, truncation, or tab-prefix collisions receive deterministic _2, _3, and later suffixes.

Multi-round tool limitations

The relay exposes browser tools as ordinary MCP tool calls. If a browser tool returns an MCP input_required result, the relay returns a tool error because it cannot proxy subsequent input rounds. Register multi-round tool flows directly on the upstream McpServer. The relay omits tools that declare execution.taskSupport: 'required'. It exposes tools that declare 'optional' or 'forbidden' as ordinary calls.

Browser embed

Add a script tag to expose a page’s WebMCP tools to the relay:
The CDN URL pins the major version because the embed and the page runtime must share a major version. Pages on a 5.x runtime load @5 for both scripts. Tools registered on document.modelContext are picked up automatically, including tools registered after the embed loads. The embed listens for toolchange and polls every two seconds as a fallback. The embed relays the page’s own tools plus tools registered by same-origin descendant frames. When several frames register the same name, the embed relays and invokes the page’s own tool (otherwise the first one getTools() returns) and logs one console warning per name. A result that parses to a JSON object becomes structured content, and an MCP result object passes through unchanged. A JSON string arrives as its content; any other result keeps its original text, so a declarative tool response of 10.50 stays 10.50. Under the polyfill and @mcp-b/global, a tool that throws returns an error result with the text Tool execution failed.

Embed attributes

Custom relay port:
Increase the per-request timeout for tools that chain several slow API calls:
The relay allows 65000 ms by default. If the page timeout is higher, start the relay with a slightly larger limit. For the 120000 ms page setting above, use --invoke-timeout 125000. The embed script fetches the sibling widget.html, injects configuration, and loads it as a hidden blob iframe that inherits the host page origin. The iframe opens a WebSocket to the relay on localhost. Self-hosted copies must serve both embed.js and widget.html; cross-origin hosts must allow the widget fetch with CORS. The relay fails closed if that fetch fails. Page code registers tools on document.modelContext. The embed discovers tools through asynchronous getTools() and executes the returned descriptor with executeTool(tool, inputObject). It does not feature-detect executeTool(): on a runtime without it, tools are listed but every call fails.

Reconnection and client mode

After a disconnect, the widget retries the last endpoint once after about 500ms, then rescans the relay range after 10s, 20s, and 30s. If no relay responds, it enters a dormant state and probes the configured or cached endpoint every two minutes. Returning to the tab or sending webmcp.connect triggers immediate rediscovery. Port range discovery and browser probing: Unless --port is explicit, the server tries ports 9333–9348 instead of failing on a single port. The chosen port is persisted to ~/.webmcp/relay-port.json for stable restarts. The widget probes the port range sequentially and caches discovered endpoints in sessionStorage. Subprotocol handshake: WebSocket connections negotiate webmcp.v1 as the primary subprotocol and webmcp-discovery.v1 for discovery probes. On connect, the server sends a server-hello message containing the relay’s identity: instanceId, host, port, relayId, and optional label and workspace. Without --relay-id, relayId defaults to the ephemeral instanceId. The browser responds with a hello message carrying tabId, origin, title, and url. The widget requires hello/accepted before it sends tools and closes the socket if no acknowledgement arrives within one second. Heartbeat: The relay server pings connected sources every 15 seconds. Connections are closed after 25 seconds with no response, enabling fast rediscovery after ungraceful relay shutdowns. Multi-relay selection: Use data-relay-id and data-relay-workspace embed attributes to filter relays during discovery. Only relays whose server-hello identity matches the configured filters are accepted. When a candidate port is owned by a compatible WebMCP relay, a second instance joins it in client mode and proxies tool operations through it. A non-relay service is skipped while scanning the default range; an explicitly selected occupied port fails. If the server relay stops, the client promotes itself back to server mode after a reconnection cycle. This lets multiple MCP clients share the same browser connections.

Runtime compatibility

The relay supports pages using:
  1. @mcp-b/global (recommended for the complete MCP-B runtime)
  2. Native Chrome with document.modelContext.getTools() and object-input executeTool()
  3. @mcp-b/webmcp-polyfill
file: pages report an opaque null origin, which the polyfill’s descriptor execution path rejects. Local integrations therefore need a non-opaque origin such as http://localhost.

Security

  • Binds to 127.0.0.1 by default (loopback only).
  • Default allowedOrigins is *, permitting any browser page to connect. Use --widget-origin to restrict which host page origins can register tools.
  • --widget-origin validates the browser’s WebSocket Origin header. The injected blob iframe inherits the host page origin, so browser connections cannot override it in hello.
  • --widget-origin is not local-process authentication. An Origin-less browser-protocol client falls back to its claimed hello.origin, while the internal relay-to-relay protocol is outside this browser-origin check. Keep the relay bound to loopback unless you add a separate trusted boundary.
  • Chrome can require Local Network Access permission before a public site opens the loopback WebSocket. This browser permission is separate from relay configuration.

Exported API

The package exports the following for programmatic use: LocalRelayMcpServerOptions accepts serverName (default webmcp-local-relay), serverVersion (default 0.0.0), launchBrowser (replaces the execFile launcher that webmcp_open_page uses), and at most one of: RelayBridgeServerOptions:

LocalRelayMcpServer members

RelayBridgeServer members

RelayRegistry members

HelloRequiredError exposes the rejected source’s read-only connectionId. Tool definition utilities exported at the package root: Message schema types and Zod schemas for the browser-relay protocol:

Troubleshooting

For common problems and their fixes, see Troubleshoot common issues.