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.
npm: @mcp-b/webmcp-local-relay
license: MIT
node: >= 22

Architecture

Browser Tab                          Local Machine
┌──────────────────────┐             ┌──────────────────────┐
│                      │  WebSocket  │                      │
│   Website with       ├────────────▶  webmcp-local-relay   │
│   WebMCP tools       │  localhost  │   (MCP server)       │
│                      │             │                      │
└──────────────────────┘             └──────────┬───────────┘

                                          stdio │ JSON-RPC

                                     ┌──────────▼───────────┐
                                     │                      │
                                     │   Claude / Cursor /  │
                                     │   any MCP client     │
                                     │                      │
                                     └──────────────────────┘
The relay has four layers:
LayerDescription
LocalRelayMcpServerMCP server exposing static and dynamic tools over stdio
RelayBridgeServerWebSocket server managing browser connections and routing calls
Widget iframeHidden iframe injected by embed.js into the host page
Host pageThe webpage with WebMCP tools registered on navigator.modelContext
For a conceptual discussion of transports and bridges, see Transports and Bridges.

Installation

Add this JSON block to your MCP client configuration:
{
  "mcpServers": {
    "webmcp-local-relay": {
      "command": "npx",
      "args": ["-y", "@mcp-b/webmcp-local-relay@latest"]
    }
  }
}
You can also run the relay directly:
npx @mcp-b/webmcp-local-relay
A .mcpb bundle file is available from GitHub Releases for Claude Desktop (double-click to install).

Minimal example

CLI options

webmcp-local-relay [options]

  --host, -H               Bind host for local websocket relay (default: 127.0.0.1)
  --port, -p               Preferred root port for the local relay cluster (default: 9333)
  --widget-origin          Allowed host page origin(s), comma-separated (default: *)
  --allowed-origin         Alias for --widget-origin
  --ws-origin              Alias for --widget-origin
  --label                  Human-readable relay label reported during discovery
  --workspace              Optional workspace name reported during discovery
  --relay-id               Stable relay identifier reported during discovery
  --help, -h               Show help
# Custom port
npx @mcp-b/webmcp-local-relay --port 9444

# Restrict to trusted origins
npx @mcp-b/webmcp-local-relay --widget-origin https://myapp.com,https://other.com

# Label and identify the relay for multi-relay selection
npx @mcp-b/webmcp-local-relay --label "dev" --workspace "myapp" --relay-id "relay-01"

Static management tools

The relay always exposes four management tools:
ToolDescription
webmcp_list_sourcesLists connected browser tabs with metadata: tab ID, origin, URL, title, icon, tool count
webmcp_list_toolsLists all relayed tools with source info
webmcp_call_toolInvokes a relayed tool by name with JSON arguments (for clients that do not support dynamic tool registration)
webmcp_open_pageOpens a URL in the default browser or refreshes a connected source page by matching origin

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:
SituationTool name
Single providerget_issue
Multiple providerssearch_ed93, search_a1b2
Tools appear and disappear as tabs open, reload, and close.

Browser embed

Add a script tag to expose a page’s WebMCP tools to the relay:
<script src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@latest/dist/browser/embed.js"></script>
If the page already registers tools on navigator.modelContext, they are picked up automatically.

Embed attributes

AttributeDefaultDescription
data-relay-host127.0.0.1Relay WebSocket host
data-relay-port9333Relay WebSocket port
data-relay-id(none)Filter relays during discovery by stable relay identifier
data-relay-workspace(none)Filter relays during discovery by workspace name
data-request-timeout60000Per-request timeout in milliseconds
data-auto-connecttrueStart discovery immediately — set to "false" to defer until an explicit webmcp.connect message
data-debug(none)Add this attribute (with no value) to enable diagnostic logging in the browser console
Custom relay port:
<script
  src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@latest/dist/browser/embed.js"
  data-relay-port="9444"
></script>
Increase the per-request timeout for tools that chain several slow API calls:
<script
  src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@latest/dist/browser/embed.js"
  data-request-timeout="120000"
></script>
The embed script injects a hidden iframe that opens a WebSocket to the relay on localhost. Tools are discovered via navigator.modelContext (or navigator.modelContextTesting as fallback) and forwarded to the relay.

Elicitation support

When the page’s model context exposes navigator.modelContext.elicitInput, the embed installs an elicitation bridge. Elicitation requests from browser tool handlers are forwarded through the relay widget iframe to the local relay server, which then forwards them to the connected MCP client (for example, Claude Code). Responses travel the same path back to the originating tool. The bridge is installed automatically before each tool invocation and only when elicitInput is present.

Reconnection and client mode

The widget reconnects automatically using exponential backoff (1.5x multiplier) from 500ms up to 3000ms, stopping after 100 attempts. Port range discovery and browser probing: 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 with a state machine (connected → retry-same-endpoint → rediscover) 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, and optional label, workspace, and relayId. The browser responds with a hello message carrying tabId, origin, title, and url. The server completes the handshake with hello/accepted. 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 second relay instance starts and the port is in use (EADDRINUSE), it falls back to client mode. In client mode the relay connects as a WebSocket client to the existing server relay and proxies tool operations through it. 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)
  2. @mcp-b/webmcp-polyfill with navigator.modelContextTesting
The browser embed uses navigator.modelContext.listTools + callTool when present, and falls back to navigator.modelContextTesting.listTools + executeTool.

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 host page origin reported in the browser hello message, not the iframe origin.
  • Any local process can connect regardless of origin restrictions.

Exported API

The package exports the following for programmatic use:
ExportKindDescription
LocalRelayMcpServerClassMCP server with stdio transport and dynamic tool sync
LocalRelayMcpServerOptionsTypeConstruction options
RelayBridgeServerClassWebSocket relay managing browser connections
RelayBridgeServerOptionsTypeBridge server options
RelayRegistryClassMulti-source tool aggregation and deduplication
AggregatedToolTypeA tool resolved across multiple sources
SourceInfoTypeMetadata about a connected browser tab
ResolvedInvocationTypeIn-flight tool call resolution state
HelloRequiredErrorClassThrown when hello handshake is missing
sanitizeNameFunctionSanitizes a tool name to [a-zA-Z0-9_]
buildPublicToolNameFunctionBuilds a disambiguated public tool name
extractSanitizedDomainFunctionExtracts and sanitizes domain from a URL
parseCliOptionsFunctionParses CLI arguments
printHelpFunctionPrints CLI usage to stderr
CliOptionsTypeParsed CLI option shape
Tool definition utilities from ./protocol:
ExportKindDescription
ToolSchemaZod schemaValidates a tool descriptor
InboundToolSchemaZod schemaValidates an incoming tool from the browser
NormalizedToolSchemaZod schemaNormalized tool shape after processing
CallToolResultSchemaZod schemaValidates a tool execution result
CallToolRequestParamsSchemaZod schemaValidates call-tool request params
ToolAnnotationsSchemaZod schemaValidates tool annotation hints
RelayInvokeArgsSchemaZod schemaValidates relayed invocation arguments
DEFAULT_TOOL_INPUT_SCHEMAconstDefault { type: 'object', properties: {} }
normalizeInboundToolFunctionNormalizes a raw inbound tool descriptor
RelayToolTypeNormalized relay-side tool shape
RelayCallToolResultTypeResult type for a relayed tool call
RelayToolAnnotationsTypeAnnotation hints for relayed tools
RelayInvokeArgsTypeType for RelayInvokeArgsSchema output
Message schema types and Zod schemas for the browser-relay protocol:
ExportKind
BrowserToRelayMessage / BrowserToRelayMessageSchemaType + Zod
RelayToBrowserMessage / RelayToBrowserMessageSchemaType + Zod
RelayClientToServerMessage / RelayClientToServerMessageSchemaType + Zod
RelayServerToClientMessage / RelayServerToClientMessageSchemaType + Zod
ServerHelloMessage / ServerHelloMessageSchemaType + Zod
RelayHelloAcceptedMessage / RelayHelloAcceptedMessageSchemaType + Zod
RelayHelloRejectedMessage / RelayHelloRejectedMessageSchemaType + Zod
RelayDescriptor / RelayDescriptorSchemaType + Zod
RelaySourceInfo / RelaySourceInfoSchemaType + Zod

Troubleshooting

ProblemFix
No sources connectedEnsure the page loaded embed.js and the relay process is running
No tools listedEnsure tools are registered on the page’s WebMCP runtime. If tools register after load, confirm your runtime emits tool-change notifications (toolschanged or registerToolsChangedCallback)
Tool not foundTab reloaded or disconnected — call webmcp_list_tools again to refresh
Connection blockedVerify --widget-origin matches your host page’s origin (e.g., https://myapp.com), and relay port matches data-relay-port
Host response timeout:The host page took longer than the per-request timeout (default 60s) to respond. Increase via data-request-timeout="<ms>" on the embed script tag