orca-webmcp
README.md
# orca-webmcp
An MCP server that exposes the [WebMCP](https://github.com/webmachinelearning/webmcp) tools of
pages open in **Orca's embedded browser** to any MCP client — Claude Code, Codex, Cursor, or an
agent running inside an Orca terminal.
No Chrome extension, no browser flag, no WebSocket bridge, no external browser.
## Why
The existing way to hand WebMCP tools to a desktop agent is the
[WebMCP Bridge](https://chromewebstore.google.com/detail/webmcp-bridge/chgjbookknohehmaocfijekhaocaanaf)
Chrome extension, which needs:
- a real Chrome install with `chrome://flags/#enable-webmcp-testing` enabled,
- the extension installed and manually activated per tab,
- `navigator.modelContextTesting`, which only exists behind that flag,
- a WebSocket server holding `127.0.0.1:12315`.
Orca already ships a Chromium browser and an `orca eval` command that runs JavaScript in a tab.
That is everything the job needs: `document.modelContext` exposes `getTools()` and `executeTool()`
directly, so tools can be read and invoked without any of the machinery above.
## How it works
```
Orca tab (page registers tools on document.modelContext)
↑ orca eval --page <id> --expression "...getTools() / executeTool()..."
orca-webmcp (this MCP server)
↑ stdio / MCP
Any MCP client
```
On every `tools/list` the server runs `orca tab list`, evaluates `document.modelContext.getTools()`
in each tab, and republishes the results as MCP tools named `tab{index}_{toolName}`. A `tools/call`
resolves that name back to a page id and runs `executeTool` there.
Tabs without WebMCP contribute nothing and never fail the listing.
## Requirements
- Node.js 18+
- The Orca app running, with its CLI on `PATH` (or set `ORCA_BIN`)
- A page that exposes `document.modelContext` — natively, or via
[`@mcp-b/webmcp-polyfill`](https://www.npmjs.com/package/@mcp-b/webmcp-polyfill)
## Install
```bash
npm install
```
## Register with a client
```bash
# Claude Code
claude mcp add --scope user --transport stdio orca-webmcp -- node /absolute/path/to/src/index.js
```
```jsonc
// Cursor — .cursor/mcp.json
{ "mcpServers": { "orca-webmcp": { "command": "node", "args": ["/absolute/path/to/src/index.js"] } } }
```
## Configuration
| Variable | Default | Purpose |
| --- | --- | --- |
| `ORCA_BIN` | `orca` | Path to the Orca CLI |
| `ORCA_WEBMCP_WORKTREE` | *(all tabs)* | Scope tools to one worktree's tabs, e.g. `active` |
| `ORCA_WEBMCP_TIMEOUT` | `30000` | Per-command timeout in ms |
## Try it
```bash
orca tab create --url https://googlechromelabs.github.io/webmcp-tools/demos/pizza-maker/
```
Then ask your agent to build a pizza. The tools appear as `tab0_add_topping`,
`tab0_set_pizza_style`, and so on.
## In-page agent (no model API key)
The same repo ships a floating chat panel that runs **inside the page**, so a user can drive a
WebMCP site without a terminal, an extension, or a model API key. The brain is a live Claude Code
session held open by the bridge, authenticated by the operator's existing login.
```
Widget in the page ──HTTP──▶ agent-server ──▶ live Claude Code session
▲ │
└──────── orca eval ◀── orca-webmcp ◀────── MCP ──────┘
```
The session stays open per tab. Spawning a process per prompt put ~10-12s of startup in front of
~3s of work; an open session pays that once. Measured end to end through the panel: 41s per prompt
before, ~10s after the first.
The turn Claude Code runs already has this project's MCP server registered, so it can call the very
tools the page exposes — the request goes out through HTTP and comes back in through the browser.
```bash
npm run agent # bridge on 127.0.0.1:8765
orca tab create --url https://googlechromelabs.github.io/webmcp-tools/demos/pizza-maker/
npm run inject -- --url pizza-maker # or --page <id>, or --all
```
The panel lists the page's tools and refreshes on `toolchange`. Each browser tab keeps its own
conversation, so follow-up prompts have context.
Orca's toolbar cannot be extended by third parties, so there is no button to click. Instead, run
the watcher and the panel mounts itself on every http(s) tab, surviving navigation:
```bash
npm run watch
```
| Variable | Default | Purpose |
| --- | --- | --- |
| `ORCA_AGENT_PORT` | `8765` | Bridge port |
| `ORCA_AGENT_IDLE_MS` | `900000` | Close a tab's session after this much silence |
### Why the bridge is not simply open
The bridge is a listening port on the operator's machine, and every page loaded in the browser can
reach `127.0.0.1`. Two things keep a hostile page from using it:
**A per-run token.** The server mints a secret at startup and writes it to
`$TMPDIR/orca-webmcp-token` with mode 0600. `inject.js` and `watch.js` read it and bake it into the
panel they mount, so a page the operator injected can call `/chat` and a drive-by page gets a 403.
**A tool allowlist.** The session is restricted to `mcp__orca-webmcp__*`. Even a request that
somehow carried a valid token can only call WebMCP tools in tabs the operator already opened — not
read files, not run commands.
> The trailing `__*` is load-bearing. `mcp__orca-webmcp` on its own matches no tool, and a turn that
> can call nothing answers as though the page had never registered any.
Nothing about the operator's Claude subscription is stored or transmitted by this project: the
Agent SDK reads Claude Code's own credentials, and the bridge only ever sees prompt text and the
session's reply.
## Making your own page reachable
Orca's Chromium does not ship WebMCP, so `document.modelContext` only exists on a page that brings
it. Load the polyfill and your tools register everywhere — Orca, Chrome with the flag, Chrome
without it:
```bash
npm i @mcp-b/webmcp-polyfill
```
```js
import { initializeWebMCPPolyfill } from '@mcp-b/webmcp-polyfill';
initializeWebMCPPolyfill(); // no-op where the browser already provides the API
document.modelContext.registerTool({
name: 'make_offer',
description: 'Offer a price for the listing. The seller accepts or counters.',
inputSchema: {
type: 'object',
properties: { amount: { type: 'number', description: 'Offer in US dollars.' } },
required: ['amount'],
},
async execute({ amount }) {
const verdict = evaluateOffer(amount); // your logic, your rules, your secrets
return { content: [{ type: 'text', text: verdict }] };
},
});
```
The polyfill costs nothing where it is not needed — it returns early when `document.modelContext`
already exists — and without it a page that guards its registration is silently invisible to every
agent:
```js
if (document.modelContext) { // undefined in Orca
document.modelContext.registerTool(...); // never runs, no tools, no error
}
```
That guard is why most of the Chrome Labs demos expose nothing here. They are showcases for the
native API written for developers running the flag, not sites built to work on a stranger's
machine — a distinction worth keeping when borrowing their structure.
Keep the polyfill until native support is the common case. The API moved from
`navigator.modelContext` to `document.modelContext` during 2026, and the npm package tracks changes
like that; a vendored copy does not.
### Registration and consumption are separate
`document.modelContext` is where a page *registers* tools. Reading and invoking them is a different
surface, and which one is available depends on the browser:
| Surface | Purpose | Where it exists |
| --- | --- | --- |
| `document.modelContext.registerTool` | a page publishes a tool | native, or via the polyfill |
| `document.modelContext.getTools` / `executeTool` | an agent reads and calls tools | alongside the above — this is what this project uses |
| `navigator.modelContextTesting` | the testing consumer API | only behind `chrome://flags/#enable-webmcp-testing` |
Extensions built against `navigator.modelContextTesting` need that flag and a Chrome new enough to
have it. Reading `getTools()` off `document.modelContext` needs neither, which is why this project
works in a browser that has no WebMCP support of its own.
## Known limits
- Tool names are keyed by **tab index**, which shifts when tabs are opened or closed. The mapping is
rebuilt on every `tools/list` and refreshed on a miss, but a call issued against a stale listing
can land on a different tab.
- Tool changes are not pushed. The server has no `tools/list_changed` notification yet, so a client
sees new tools on its next listing.
- Orca's Chromium (Chrome 150 as of app 1.4.190) does **not** ship WebMCP natively; pages must load
the polyfill themselves. All Chrome Labs demos do.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues