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.

Minimal example

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

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 through the feature-detected, serialized-JSON executeTool() signature supported by the package and current Chrome implementation.

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 the serialized-JSON executeTool() signature that this relay requires
  3. @mcp-b/webmcp-polyfill
The browser embed always uses document-first discovery and executes the current descriptor returned by getTools(). It does not depend on the removed name-based compatibility APIs. 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), 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