Docs Guide

Guide

Expose live client applications as MCP servers.

A browser, desktop or mobile application opens one outbound WebSocket to a ClientMCP bridge and declares tools. AI agents use them through the bridge's standard MCP endpoint. The tools run inside the application, on the same state and actions as its UI — when an agent adds a todo, the user sees it appear.

 Human ──► UI ─┐
               ▼
        Application state & actions ◄── ClientMCP SDK
                                              │  WebSocket, outbound only
                                              ▼
                                       ClientMCP bridge  ── Docker, or Cloudflare Workers
                                              ▲
                                              │  MCP Streamable HTTP (2026-07-28 and 2025 revisions)
                                           AI agent
  • No server in the app. The application opens no port; the bridge stores nothing.
  • Standard MCP. Any MCP client works — the bridge speaks MCP 2026-07-28 and the 2025 revisions on one endpoint.
  • Two deployments, one protocol. A 1.2 MB static binary for any Linux host, or Cloudflare Workers with Durable Objects, where idle applications cost nothing. Both share one Rust core and pass the same conformance suite.

Quick start

1. Run a bridge.

docker build -t clientmcp .
export CLIENTMCP_SECRET=$(openssl rand -hex 32)
docker run -d -p 8080:8080 -e CLIENTMCP_SECRET clientmcp

2. Mint tokens. A client token lets the application connect as my-app; an agent token lets an agent call it.

docker run --rm -e CLIENTMCP_SECRET clientmcp token client my-app
docker run --rm -e CLIENTMCP_SECRET clientmcp token agent my-app --ttl 30d

3. Expose functions from the application (SDK reference):

import { ClientMCP } from "clientmcp";

const mcp = new ClientMCP({ url: "ws://localhost:8080/connect", token: CLIENT_TOKEN });
mcp.expose("add_todo", {
  description: "Add a todo",
  inputSchema: { type: "object", properties: { title: { type: "string" } }, required: ["title"] },
  handler: ({ title }) => todos.add(title), // the same action the UI calls
});
await mcp.connect();

4. Connect an agent, e.g. Claude Code:

claude mcp add --transport http my-app http://localhost:8080/mcp \
  --header "Authorization: Bearer $AGENT_TOKEN"

Hosts that cannot send headers can use the token as the last path segment: https://bridge.example.com/mcp/<agent token> — such a URL is a secret.

examples/browser is a complete application. Build the SDK (cd sdk/js && npm install && npm run build), serve the repository root with any static server, and open examples/browser/index.html?bridge=ws://localhost:8080/connect&token=<client token>.

Deploy

Linux / Docker

The image is a static binary on scratch, running as an unprivileged user. Put TLS in front of it; deploy/ has a Compose file with Caddy that obtains certificates automatically. Without Docker: cargo build --release -p clientmcp-server and run target/release/clientmcp.

Cloudflare Workers

cargo install worker-build
cd crates/worker
npx wrangler secret put CLIENTMCP_SECRET
npx wrangler deploy

One Durable Object per client ID holds its WebSocket through the Hibernation API; pings are answered at the edge and agents' notification streams are held by the Worker, so a connected but idle application does not keep anything awake.

Configuration

Variable Default Meaning
CLIENTMCP_SECRET — (required) Token secrets, ≥ 32 bytes, newest first, space-separated for rotation.
CLIENTMCP_CALL_TIMEOUT 120 Seconds before an unanswered call fails and is cancelled.
CLIENTMCP_ALLOWED_ORIGINS none Browser origins allowed on /mcp (comma-separated, or *).
CLIENTMCP_LISTEN 127.0.0.1:8080 Listen address (server only; the image uses 0.0.0.0:8080).

Endpoints: /connect (applications), /mcp and /mcp/<token> (agents), /health.

Tokens

Tokens are HS256 JWTs, so an application backend can mint them with any JWT library:

{ "sub": "my-app", "role": "client", "exp": 1798761600 }

sub is the client ID: a token for my-app can only connect as, or call, my-app. In production, have your backend issue short-lived client tokens (the SDK accepts a token function, called on every reconnection) and give agents tokens with an exp. See SPEC.md §2 and §7.

Protocol

SPEC.md is the normative specification; docs/design.md explains the decisions behind it. In short, the client channel is six JSON messages:

client → bridge   register {tools, instructions}   result {id, result}   ping
bridge → client   registered   call {id, name, arguments}   cancel {id}   pong

The bridge maps them onto MCP: tools/list returns the declared tools, tools/call becomes a call, and every change is pushed as notifications/tools/list_changed. An offline application simply has no tools; agents stay connected and are notified when it returns.

Verification

cargo test --workspace --exclude clientmcp-worker   # core unit tests
tests/run-native.sh                                  # conformance suite vs. the server
tests/run-worker.sh                                  # conformance suite vs. the Worker in workerd

The conformance suite (tests/, 33 cases) drives a bridge with the official MCP SDKs as agents — v1 (2025 era) and v2 (2026-07-28, pinned and auto-negotiated) — and this repository's SDK as the application. It covers authentication, origin checks and CORS, both MCP eras, header validation, x-mcp-header, notification streams, streamed slow calls, concurrency, timeouts, cancellation in both eras (checked before the call timeout could cancel instead), disconnection, replacement, size limits and tool validation. It passes unchanged against the native server, the Docker image and the Worker running in workerd. Beyond the suite: the browser example was exercised end to end in Chrome — agent changes appeared in the UI, user edits were visible to the agent, a declined confirmation reached the agent as a tool error, and the page reconnected and re-registered after a bridge restart — and Claude Code, as a real agent, listed and added todos in a running application through the bridge.

Performance

clientmcp-bench (in crates/bench) runs echo clients and concurrent agents over keep-alive HTTP. Numbers below are from loopback runs with the load generator on the same machine, so they understate what the bridge alone can do.

Measure (Linux, aarch64, static binary) Result
Bridge CPU per tools/call round trip, 2025 / 2026-07-28 MCP ~11 µs / ~14 µs
Bridge CPU per tools/list ~5.5 µs
Bridge CPU per call with 64 KiB arguments and result ~110 µs
One call, unloaded p50 54 µs, p99 134 µs
64 agents → 1 client, 2 vCPUs shared with the load generator 86–93k calls/s, p99 ≈ 1.9 ms
256 agents → 64 clients, same 71–85k calls/s, p99 8–11 ms
Memory: idle process / per idle connected client 1.2 MB / ~11 KB
CPU while 10,000 clients sit idle (pinging every 25 s) 0

On an Apple M2 Pro (macOS, 12 cores shared with the load generator), 64 agents calling one client sustain 81–90k calls/s with p99 ≈ 1.4–2.2 ms.

The Worker passes the same suite in workerd, but wrangler dev's local proxy caps throughput at ~1.2k requests/s, so local numbers say nothing about Cloudflare's network; measure a deployed Worker for that.

Repository

SPEC.md            the protocol
crates/core        protocol logic shared by both runtimes, no I/O
crates/server      the Linux/Docker bridge (tokio, hyper, tokio-tungstenite)
crates/worker      the Cloudflare bridge (workers-rs, Durable Objects)
sdk/js             the TypeScript SDK for applications
tests              the conformance suite
crates/bench       the load generator
examples/browser   a shared todo list
deploy             Compose + Caddy

Limitations

  • Tokens can be revoked before exp only by rotating the secret.
  • The server bridge is a single instance: several instances need requests for one client ID routed to the same instance.
  • The bridge sees all arguments and results; it must be trusted.
  • v1 covers tools. Progress notifications, resources, prompts and MCP's multi round-trip requests are not implemented.

License

MIT or Apache-2.0, at your option.