tab-mcp
MCP tools for static websites and single-page apps, without a backend.
A page registers tools with the browser library
@ractive-ch/tab-mcp-client. It has no server to run an MCP server on,
so it opens a WebSocket to a tab-mcp relay. An AI app (Claude, ChatGPT, Claude Code
or any other MCP client) connects to the same relay over MCP. The relay passes the
AI app's requests to the page and the page's answers back. The tools run in the
browser tab, on the page's own state, while the tab is open.
How it works
Both sides connect out to the relay; neither has to be reachable from the internet.
The page connects with a WebSocket (protocol tab-mcp.v1) and gets its
own MCP endpoint on the relay, /a/{id}/mcp. The AI app uses that
endpoint like any remote MCP server: Streamable HTTP, authorized with OAuth. The
authorization asks for a six-digit code that only the page shows, so only the person
in front of the tab can pair an AI app with it.
Web apps with a login can pair by identity instead of a code
(TAB_MCP_IDENTITY=oidc). The web app and the relay are then both
clients of the same OpenID Connect provider (Google, Keycloak, Auth0, Okta,
Microsoft Entra ID or your own). You are signed in to the web app with your account
there. When an AI app connects, you sign in to the relay with the same account,
usually without having to log in again, because the provider still knows you. With any
other account the relay pairs nothing, and logging out of the web app ends the
connection. To tell the relay who is signed in to the page,
the web app's backend (an edge function is enough) signs a short assertion. See
identity binding.
The relay knows no tools. It checks the AI app's token and its limits and forwards
each MCP request over the WebSocket; the page answers tools/list and
tools/call itself. When the tab closes, the tools are gone and the AI
app gets "the page is not open". It stays paired for the next time the page connects.
Demo
The connection step by step
Register tools
Each registerTool call adds an MCP tool. Its name, description, input
schema and annotations are what the AI app gets from tools/list, and
execute runs in the page when the AI app calls the tool.
consequentialHint is the library's own: the AI app sees it as MCP's
destructiveHint, and the page asks the user before each call.
import { TabMcp } from "@ractive-ch/tab-mcp-client";
const notes: string[] = [];
const mcp = new TabMcp({
relay: "https://relay.example.com",
server: { name: "notes", version: "1.0.0" },
appName: "Notes",
});
// Reads only: the AI app may call it whenever it likes.
await mcp.registerTool({
name: "list_notes",
description: "List the notes on the page",
annotations: { readOnlyHint: true },
execute: () => notes,
});
// Changes the page: the page asks you before each call.
await mcp.registerTool<{ text: string }>({
name: "add_note",
description: "Add a note to the list on the page",
inputSchema: {
type: "object",
properties: { text: { type: "string" } },
required: ["text"],
},
annotations: { consequentialHint: true },
execute: ({ text }) => {
notes.push(text);
return `Added. The page has ${notes.length} notes.`;
},
});
// Your Connect button opens the dialog with the address and the code.
document.querySelector("#connect")?.addEventListener("click", () => mcp.showConnectDialog());
The library also loads from a CDN without a bundler and has React bindings for single-page apps. See the quick start for page authors and the library's README.
Run your own relay
The relay at tab-mcp.ractive.ch accepts pages from ractive.ch only. For
your own sites, run a relay: one process with a SQLite file, published as the Docker
image ractive/tab-mcp (also ghcr.io/ractive/tab-mcp, for
linux/amd64 and linux/arm64) and on npm as @ractive-ch/tab-mcp.
docker run -d -p 127.0.0.1:8080:8080 -v tab-mcp-data:/data \
-e TAB_MCP_PUBLIC_URL=https://relay.example.com \
-e TAB_MCP_ALLOWED_ORIGINS=https://docs.example.com,https://app.example.org \
ractive/tab-mcp
One relay serves pages from every origin in TAB_MCP_ALLOWED_ORIGINS,
across domains, and each connected page gets its own MCP endpoint. It works like one
MCP server shared by all your static sites, where every open tab brings its own
tools. Put the relay behind a reverse proxy with TLS:
self-hosting
covers WebSockets, event streams, proxy time limits and every setting.
When to use something else
tab-mcp is for state that lives in the browser (a drawing, an emulator, a form being filled in) and for acting as the signed-in user in their open tab. If a site's state lives on its server, an MCP server on that backend fits better: it works with no tab open, needs no relay and can check who calls with the site's own accounts.
Examples
-
Diagram editor
Boxes and arrows your AI app draws, reads and rearranges with you, with example prompts to try.
-
Todo list
A simple todo list, an example of a React application with the tab-mcp React library.
-
saturnus
HP calculator emulators in the browser that your AI app can use.
Connect an AI app
How to add a page to Claude, ChatGPT, Codex or Claude Code, and what to do when it fails: connecting an AI app.
The hosted relay's privacy notice. Questions and bugs: GitHub issues.