> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mcp-b.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# @mcp-b/webmcp-local-relay reference

> Local MCP relay server that bridges WebMCP tools from browser tabs to desktop AI clients over stdio.

`@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.

```text title="Package metadata" theme={null}
npm: @mcp-b/webmcp-local-relay
license: MIT
node: >= 22
```

## Architecture

```text theme={null}
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:

| Layer | Description |
| - | - |
| `LocalRelayMcpServer` | MCP server exposing static and dynamic tools over stdio |
| `RelayBridgeServer` | WebSocket server managing browser connections and routing calls |
| Widget iframe | Hidden iframe injected by `embed.js` into the host page |
| Host page | The webpage with WebMCP tools registered on `document.modelContext` |

For a conceptual discussion of transports and bridges, see [Transports and Bridges](/explanation/architecture/transports-and-bridges).

## Installation

Add this JSON block to your MCP client configuration:

```json "MCP client config" theme={null}
{
  "mcpServers": {
    "webmcp-local-relay": {
      "command": "npx",
      "args": ["-y", "@mcp-b/webmcp-local-relay@latest"]
    }
  }
}
```

You can also run the relay directly:

```bash theme={null}
npx @mcp-b/webmcp-local-relay
```

A `.mcpb` bundle file is available from [GitHub Releases](https://github.com/WebMCP-org/npm-packages/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

```text theme={null}
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         Deprecated alias for --widget-origin
  --ws-origin              Deprecated 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
  --invoke-timeout         Browser tool invocation timeout in milliseconds (default: 65000)
  --max-payload            Maximum WebSocket payload size in bytes (default: 10000000)
  --help, -h               Show help
```

```bash theme={null}
# 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 three management tools:

| Tool | Description |
| - | - |
| `webmcp_list_sources` | Lists connected browser tabs that publish tools, with tab metadata |
| `webmcp_list_tools` | Lists all relayed tools with source info |
| `webmcp_open_page` | Opens a URL, or in server mode 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:

| Situation | Tool name |
| - | - |
| Single provider | `get_issue` |
| Multiple providers | `search_ed93`, `search_a1b2` |

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:

```html theme={null}
<script src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@6/dist/browser/embed.js"></script>
```

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

| Attribute | Default | Description |
| - | - | - |
| `data-relay-host` | `127.0.0.1` | Loopback relay host: `127.0.0.1`, `localhost`, or IPv6 loopback |
| `data-relay-port` | `9333` | Relay 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-timeout` | `60000` | Per-request timeout in milliseconds |
| `data-auto-connect` | `true` | Start 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:

```html theme={null}
<script
  src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@6/dist/browser/embed.js"
  data-relay-port="9444"
></script>
```

Increase the per-request timeout for tools that chain several slow API calls:

```html theme={null}
<script
  src="https://cdn.jsdelivr.net/npm/@mcp-b/webmcp-local-relay@6/dist/browser/embed.js"
  data-request-timeout="120000"
></script>
```

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](https://developer.chrome.com/release-notes/147) 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:

| Export | Kind | Description |
| - | - | - |
| `LocalRelayMcpServer` | Class | MCP server with stdio transport and dynamic tool sync |
| `LocalRelayMcpServerOptions` | Type | Construction options |
| `RelayBridgeServer` | Class | WebSocket relay managing browser connections |
| `RelayBridgeServerOptions` | Type | Bridge server options |
| `RelayRegistry` | Class | Multi-source tool aggregation and deduplication |
| `AggregatedTool` | Type | A tool resolved across multiple sources |
| `SourceInfo` | Type | Metadata about a connected browser tab |
| `ResolvedInvocation` | Type | Provider selected as an invocation target |
| `HelloRequiredError` | Class | Thrown when hello handshake is missing |
| `sanitizeName` | Function | Sanitizes a tool name to `[a-zA-Z0-9_]` |
| `buildPublicToolName` | Function | Builds a disambiguated public tool name |
| `extractSanitizedDomain` | Function | Extracts and sanitizes domain from a URL |
| `parseCliOptions` | Function | Parses CLI arguments |
| `printHelp` | Function | Prints CLI usage to stderr |
| `CliOptions` | Type | Parsed CLI option shape |

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

| Field | Type | Description |
| - | - | - |
| `bridge` | `RelayBridgeServer` | Use an existing bridge instance |
| `bridgeOptions` | `RelayBridgeServerOptions` | Construct and own a bridge instance |

`RelayBridgeServerOptions`:

| Field | Type | Default |
| - | - | - |
| `host` | `string` | `127.0.0.1` |
| `port` | `number` | `9333` |
| `portExplicitlySet` | `boolean` | `false` |
| `portRangeEnd` | `number` | `9348` |
| `persistPath` | `string` | `~/.webmcp/relay-port.json` |
| `allowedOrigins` | `string[]` | `['*']` |
| `maxPayloadBytes` | `number` | `10000000` |
| `invokeTimeoutMs` | `number` | `65000` |
| `label` | `string` | *(none)* |
| `workspace` | `string` | *(none)* |
| `relayId` | `string` | Generated relay `instanceId` |

### `LocalRelayMcpServer` members

| Member | Returns | Description |
| - | - | - |
| `new LocalRelayMcpServer(options?)` | `LocalRelayMcpServer` | Creates a server with an owned or supplied bridge |
| `bridge` | `RelayBridgeServer` | Underlying browser bridge |
| `start()` | `Promise<void>` | Starts the bridge and synchronizes dynamic tools |
| `connect(transport)` | `Promise<void>` | Connects one MCP transport; callable once per instance lifecycle |
| `startStdio()` | `Promise<void>` | Connects the MCP server through stdio |
| `stop()` | `Promise<void>` | Closes the MCP transport and bridge |
| `listDynamicToolNames()` | `string[]` | Returns sorted dynamic tool names |

### `RelayBridgeServer` members

| Member | Returns | Description |
| - | - | - |
| `new RelayBridgeServer(options?, registry?)` | `RelayBridgeServer` | Creates a bridge with an optional supplied registry |
| `registry` | `RelayRegistry` | Registry used by server mode |
| `mode` | `'server' \| 'client'` | Current operating mode |
| `port` | `number` | Bound server port or upstream relay port |
| `listToolsFromRelay()` | `RelayTool[]` | Upstream tools in client mode; empty in server mode |
| `listSourcesFromRelay()` | `RelaySourceInfo[]` | Upstream sources in client mode; empty in server mode |
| `getToolSourceMapFromRelay()` | `Record<string, string[]>` | Upstream public-tool-to-source mapping |
| `start()` | `Promise<void>` | Starts with the configured port strategy |
| `stop()` | `Promise<void>` | Stops server, client, timers, and pending invocations |
| `reloadSource(connectionId)` | `void` | Reloads a connected source; server mode only |
| `invokeTool(toolName, args, options?)` | `Promise<RelayCallToolResult>` | Routes an invocation locally or through the upstream relay |

### `RelayRegistry` members

| Member | Returns | Description |
| - | - | - |
| `new RelayRegistry(now?)` | `RelayRegistry` | Creates an in-memory registry with an optional time provider |
| `upsertSource(connectionId, hello)` | `void` | Creates or updates source metadata |
| `registerTools(connectionId, tools)` | `void` | Replaces a source's complete tool set |
| `removeConnection(connectionId)` | `void` | Removes a source and its tools |
| `touchConnection(connectionId)` | `void` | Updates the source's last-seen time |
| `listSources()` | `SourceInfo[]` | Lists active sources that publish tools |
| `listTools()` | `AggregatedTool[]` | Lists aggregated tools by public name |
| `resolveInvocation(options)` | `ResolvedInvocation \| null` | Selects a provider by source, tab, or recency |

`HelloRequiredError` exposes the rejected source's read-only `connectionId`.

**Tool definition utilities exported at the package root:**

| Export | Kind | Description |
| - | - | - |
| `ToolSchema` | Zod schema | Validates a tool descriptor |
| `InboundToolSchema` | Zod schema | Validates an incoming tool from the browser |
| `NormalizedToolSchema` | Zod schema | Normalized tool shape after processing |
| `CallToolResultSchema` | Zod schema | Validates a tool execution result |
| `CallToolRequestParamsSchema` | Zod schema | Validates call-tool request params |
| `ToolAnnotationsSchema` | Zod schema | Validates tool annotation hints |
| `RelayInvokeArgsSchema` | Zod schema | Validates relayed invocation arguments |
| `DEFAULT_TOOL_INPUT_SCHEMA` | const | Default `{ type: 'object', properties: {} }` |
| `normalizeInboundTool` | Function | Normalizes a raw inbound tool descriptor |
| `RelayTool` | Type | Normalized relay-side tool shape |
| `RelayCallToolResult` | Type | Result type for a relayed tool call |
| `RelayToolAnnotations` | Type | Annotation hints for relayed tools |
| `RelayInvokeArgs` | Type | Type for `RelayInvokeArgsSchema` output |

**Message schema types and Zod schemas** for the browser-relay protocol:

| Export | Kind |
| - | - |
| `BrowserToRelayMessage` / `BrowserToRelayMessageSchema` | Type + Zod |
| `RelayToBrowserMessage` / `RelayToBrowserMessageSchema` | Type + Zod |
| `RelayClientToServerMessage` / `RelayClientToServerMessageSchema` | Type + Zod |
| `RelayServerToClientMessage` / `RelayServerToClientMessageSchema` | Type + Zod |
| `ServerHelloMessage` / `ServerHelloMessageSchema` | Type + Zod |
| `RelayHelloAcceptedMessage` / `RelayHelloAcceptedMessageSchema` | Type + Zod |
| `RelayHelloRejectedMessage` / `RelayHelloRejectedMessageSchema` | Type + Zod |
| `RelayDescriptor` / `RelayDescriptorSchema` | Type + Zod |
| `RelaySourceInfo` / `RelaySourceInfoSchema` | Type + Zod |

## Troubleshooting

For common problems and their fixes, see [Troubleshoot common issues](/how-to/connect-desktop-agents-with-local-relay#troubleshoot-common-issues).

## Related

* [Connect Desktop Agents with Local Relay](/how-to/connect-desktop-agents-with-local-relay) (how-to guide)
* [Desktop Agent Relay](/tutorials/desktop-agent-relay) (tutorial)
* [@mcp-b/global](/packages/global/reference) (runtime reference)
* [Transports and Bridges](/explanation/architecture/transports-and-bridges) (explanation)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.