@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
For a conceptual discussion of transports and bridges, see Transports and Bridges.
Installation
Add this JSON block to your MCP client configuration:.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 MCPinput_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: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:
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 about500ms, 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:@mcp-b/global(recommended for the complete MCP-B runtime)- Native Chrome with
document.modelContext.getTools()and the serialized-JSONexecuteTool()signature that this relay requires @mcp-b/webmcp-polyfill
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.1by default (loopback only). - Default
allowedOriginsis*, permitting any browser page to connect. Use--widget-originto restrict which host page origins can register tools. --widget-originvalidates the browser’s WebSocketOriginheader. The injected blob iframe inherits the host page origin, so browser connections cannot override it inhello.--widget-originis not local-process authentication. An Origin-less browser-protocol client falls back to its claimedhello.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
Related
- Connect Desktop Agents with Local Relay (how-to guide)
- Desktop Agent Relay (tutorial)
- @mcp-b/global (runtime reference)
- Transports and Bridges (explanation)
