Skip to content

tab-mcp

MCP tools for static websites and single-page apps, without a backend.

tab-mcp: a browser tab and an AI app, connected through a relay

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.

Source on GitHub

How it works

Two pages, a static site and a single-page app, each connect to the tab-mcp relay with a WebSocket. Claude, ChatGPT, Codex and Claude Code connect to the same relay over MCP.WebSocketWebSocketMCPMCPStatic sitedocs.example.comSingle-page appapp.example.orgtab-mcp relayClaudeChatGPT, Codex,Claude Code

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 diagram editor is connected to Claude Desktop: the page's address goes into Claude as a custom connector, and the relay pairs the two with the code the page shows. Claude then lists the page's tools and calls them. 45 seconds, no sound; waiting is cut and Claude's work is sped up.

tab-mcp demo

The connection step by step

How tab-mcp works: you connect the page, pair your AI app with the code the page shows, the AI app calls the page's tools through the relay, and the tools are gone when you close the tab.opt[the tool changes something]YouWeb page in yourtab(tab-mcp-client)tab-mcp relayAI app(Claude, ChatGPT,Claude Code)1. Connect the pageclick "Connect your AIapp"WebSocket /tab(tab-mcp.v1)address /a/{id}/mcp and a6-digit codeshow the address and thecode2-3. Pair the AI appadd the addressopen the relay's /authorize page in your browsercheck the AI app and the page, type the codetoken for this addressgrant.addedlist the AI app asconnected, withDisconnect4. Call a toolPOST /a/{id}/mcp with thetoken: tools/callchecks the token and limits, knows notools, stores no argumentshttp.request over theWebSocketask firstallowthe tool's result,streamed over theWebSocketthe result5. Close the tabclose the tabWebSocket closedtools/call with the token"The page is not open"(the AI app stays paired)
Connect, pair, call a tool, close the tab. Codes, tokens and the relay's endpoints: how it works on GitHub.

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

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.