No server in the app
The application opens no port and runs no HTTP server. One outbound WebSocket, so a browser tab behind NAT works as well as a desktop app.
Open protocol v1.0 · MIT / Apache-2.0
ClientMCP exposes a live browser, desktop or mobile application as a standard MCP server. Agents call the same actions as its UI, on the same state. When an agent adds a todo, the user sees it appear.
01 The idea
Agents usually reach an application from the outside: screenshots and simulated clicks, or a separate backend API that cannot see what is open in the app. ClientMCP works from the inside. The tool you expose is the function your button already calls.
// One implementation of each action…
const addTodo = (title: string) => todos.add(title);
// …called by the UI, for people,
form.onsubmit = () => addTodo(input.value);
// …and by agents, through ClientMCP.
mcp.expose("add_todo", {
description: "Add a todo",
inputSchema: {
type: "object",
properties: { title: { type: "string" } },
required: ["title"],
},
handler: ({ title }) => addTodo(title),
}); 02 How it works
The application opens one WebSocket to a bridge and declares its tools. Agents talk to the same bridge over standard MCP Streamable HTTP. Neither needs to be reachable, and the bridge stores nothing.
Declares tools, receives calls and runs them in the app. Pings, reconnects with backoff and re-registers on its own. No dependencies.
Translates between the WebSocket and MCP. One Rust core with two runtimes: a 1.2 MB static binary for any Linux host, or Cloudflare Workers with Durable Objects.
Needs nothing beyond MCP and a bearer token. tools/list returns what the app declared; tools/call runs inside the app.
03 Quick start
A static binary in a scratch image. It stores nothing; the secret signs every token.
git clone https://github.com/atshelchin/clientmcp && cd clientmcp
docker build -t clientmcp .
export CLIENTMCP_SECRET=$(openssl rand -hex 32)
docker run -d -p 8080:8080 -e CLIENTMCP_SECRET clientmcp 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 The SDK in sdk/js has no dependencies and runs in browsers, Node.js 22+, Deno and Bun.
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(); Any MCP client works. Hosts that cannot send headers can put the token in the URL: /mcp/<agent token>.
claude mcp add --transport http my-app http://localhost:8080/mcp \
--header "Authorization: Bearer $AGENT_TOKEN" Read the full guide for production tokens, TLS and configuration, or open the browser example, a complete shared todo list.
04 The details
The application opens no port and runs no HTTP server. One outbound WebSocket, so a browser tab behind NAT works as well as a desktop app.
The bridge serves MCP 2026-07-28 and the 2025 revisions side by side, decided per request, keeping no per-agent state in either.
A closed app simply has no tools. Agents stay connected and get tools/list_changed the moment it comes back.
A call still running after a second streams as SSE with keep-alives, so a user can take a minute to confirm without a proxy cutting it off.
HS256 JWTs with a client ID and a role. Any backend can mint them and any bridge verifies them without a database. Secrets rotate without downtime.
Each register declares the complete tool set and the bridge mirrors it. There are no incremental updates to lose or reorder.
05 Control
Arguments come from a language model, and prompt injection can steer it. So the app decides which tools exist, validates every argument and asks its user before anything consequential. Two optional conventions let every app say so in the same words.
Published as _meta with the MCP annotations each level implies.
The change held, it started and goes on, or nothing changed and why.
import { completed, rejected } from "clientmcp";
mcp.expose("clear_done", {
description: "Remove completed todos",
risk: "manage", // observe | operate | manage | transact
handler: async () => {
if (!(await askTheUser("Remove completed items?")))
return rejected("denied", "the user said no");
todos.clearDone();
return completed(); // the change holds — the app has seen it
},
}); 06 Protocol
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. Tool definitions, arguments and results pass
through verbatim, so new MCP fields need no bridge release.
Read the specification: tokens, close codes, both MCP eras, limits and security, in one page.
07 Deploy
A 1.2 MB static binary on scratch, running as an unprivileged user. Put TLS
in front; the repository has a Compose file with Caddy that gets certificates
automatically.
docker run -d -p 8080:8080 -e CLIENTMCP_SECRET clientmcp
# or, without Docker
cargo build --release -p clientmcp-server One Durable Object per client ID holds its WebSocket through the Hibernation API. Pings are answered at the edge, so a connected but idle app keeps nothing awake.
cd crates/worker
npx wrangler secret put CLIENTMCP_SECRET
npx wrangler deploy 08 Measured
Static binary on Linux aarch64, loopback, with the load generator on the same machine, so the numbers understate the bridge alone. Full table.
The conformance suite drives a bridge with the official MCP SDKs as agents (v1 for the 2025 era, v2 for 2026-07-28) and passes unchanged against the native server, the Docker image and the Worker running in workerd. Beyond the suite, the browser example was run end to end in Chrome, and Claude Code, as a real agent, listed and added todos through the bridge.
09 Limits
It terminates TLS and sees every argument and result. There is no end-to-end encryption between agent and app: run a bridge you operate or trust.
Before exp, only rotating the secret revokes a token. Several secrets can be active at once, so rotating needs no downtime.
The single-process bridge holds tens of thousands of clients, but several instances need requests for a client routed to one of them. Workers scale by construction.
Resources, prompts, progress notifications and multi round-trip requests are not in v1. They fit later as new message types.