MCP Server
Everything here describes the server that opentabs start runs on your machine. It's a single localhost HTTP process — port 9515 by default. AI clients connect to /mcp, the Chrome extension connects to /ws. No cloud, no relay, no accounts. Just a process running locally, bridging your AI agent and your browser.
HTTP Endpoints
The server runs on port 9515 by default (override with the PORT environment variable or --port flag).
| Endpoint | Method(s) | Auth | Description |
|---|---|---|---|
/mcp | POST, GET, DELETE | Bearer token | MCP Streamable HTTP transport — AI clients connect here. |
/ws | WebSocket upgrade | Sec-WebSocket-Protocol | Chrome extension connection (full-duplex JSON-RPC). |
/ws-info | GET | Bearer token | Returns WebSocket URL for extension bootstrap. |
/health | GET | Optional Bearer token | Server health, plugin status, and metrics. Authenticated requests return the full payload. |
/audit | GET | Bearer token | Tool invocation history (last 500 entries). |
/reload | POST | Bearer token, rate-limited | Trigger config reload and plugin rediscovery. |
/extension/reload | POST | Bearer token, rate-limited | Signal the Chrome extension to reload. |
/mcp — MCP Streamable HTTP
The primary endpoint for MCP clients. Implements the MCP Streamable HTTP transport.
| Header | Required | Description |
|---|---|---|
Authorization | Yes (if secret configured) | Bearer <secret> — the 64-char hex token from ~/.opentabs/extension/auth.json. |
mcp-session-id | After initialization | Session UUID returned on the initial initialize request. Include in all subsequent requests. |
Multiple concurrent MCP clients are supported — each gets an independent session. DELETE closes a session.
MCP Capabilities
The server exposes these MCP capabilities:
- Tools — plugin tools (prefixed as
<plugin>_<tool>) and built-in browser tools (browser_*,extension_*) - Logging — plugin log entries forwarded as
notifications/messageto connected clients - Progress — tool progress notifications forwarded as
notifications/progress(requiresprogressTokenin request_meta)
/ws — WebSocket
The Chrome extension connects here for full-duplex JSON-RPC 2.0 communication. Only one active extension connection is allowed — a new connection replaces the previous one.
The extension authenticates via the Sec-WebSocket-Protocol header:
Sec-WebSocket-Protocol: opentabs, <secret>
Max payload: 10 MB.
/health — Health check
Returns server status. Accepts an optional Bearer token — unauthenticated requests receive only the status field, authenticated requests receive the full payload.
Unauthenticated response
{
"status": "ok"
}Authenticated response
Pass Authorization: Bearer <secret> to get the full payload:
{
"status": "ok",
"version": "0.0.4",
"sdkVersion": "0.0.4",
"mode": "production",
"extensionConnected": true,
"extensionConnections": 1,
"mcpClients": 1,
"plugins": 2,
"pluginDetails": [
{
"name": "slack",
"displayName": "Slack",
"toolCount": 22,
"tools": ["slack_send_message", "slack_list_channels"],
"tabState": "ready",
"tabs": [
{ "tabId": 1, "url": "https://app.slack.com/client", "title": "Slack", "ready": true }
],
"iconSvg": "<svg>...</svg>",
"source": "local",
"sdkVersion": "0.0.4",
"logBufferSize": 42
}
],
"failedPlugins": [],
"toolCount": 59,
"browserToolCount": 40,
"pluginToolCount": 19,
"browserToolNames": ["browser_navigate_tab", "browser_click_element", "browser_screenshot_tab"],
"disabledBrowserTools": ["browser_execute_script"],
"skipPermissions": false,
"uptime": 3600,
"reloadCount": 2,
"lastReloadTimestamp": 1740134100000,
"lastReloadDurationMs": 45,
"stateSchemaVersion": 6,
"discoveryErrors": [],
"fileWatcher": {
"watchedPlugins": 1,
"pendingPlugins": 0,
"lastPollAt": 1740134100000,
"pollDetections": 0
},
"auditSummary": {
"totalInvocations": 150,
"successCount": 142,
"failureCount": 8,
"last24h": { "total": 50, "success": 47, "failure": 3 },
"avgDurationMs": 312
}
}Key fields:
mode—"dev"or"production"extensionConnections— number of active extension WebSocket connectionssdkVersion— server-level SDK versionbrowserToolCount— number of built-in browser tools registeredpluginToolCount— total number of plugin tools across all loaded pluginsbrowserToolNames— all registered browser tool names (including disabled ones)pluginDetails[].tools— prefixed tool names exposed by this plugin (e.g.,["slack_send_message"])pluginDetails[].tabs— array of currently matching tabs:{ tabId, url, title, ready }per tabpluginDetails[].iconSvg— SVG markup for the plugin icon (present only if the plugin provides one)pluginDetails[].sdkVersion— per-plugin SDK version (null if missing)pluginDetails[].logBufferSize— number of log entries in the per-plugin ring bufferfailedPlugins— plugins that failed to load (path + error message)disabledBrowserTools— browser tool names that have been disabled via configskipPermissions— whether all permission checks are bypassed (viaOPENTABS_DANGEROUSLY_SKIP_PERMISSIONS=1env var)lastReloadTimestamp— milliseconds since epoch of the last plugin reload (0 if never reloaded)lastReloadDurationMs— how long the last reload took in millisecondsstateSchemaVersion— internal state schema version numberdiscoveryErrors— array of non-fatal errors encountered during plugin discoveryfileWatcher— file watcher status (dev mode only):watchedPlugins(actively watched),pendingPlugins(not yet watched),lastPollAt(last mtime poll timestamp, null if no poll yet),pollDetections(changes detected via polling)auditSummary— aggregate stats from the in-memory audit log, includinglast24hbreakdown andavgDurationMs
/ws-info — WebSocket bootstrap
Returns the WebSocket URL for the Chrome extension to connect. Requires Bearer token authentication.
curl -s http://127.0.0.1:9515/ws-info \
-H "Authorization: Bearer <secret>"Response:
{
"wsUrl": "ws://127.0.0.1:9515/ws"
}The extension uses this URL to establish the WebSocket connection. The wsUrl is derived from the server's host and always points to the /ws endpoint.
/audit — Tool invocation history
Returns tool invocations as a JSON array, newest first. Requires Bearer token authentication. The server maintains a circular buffer of the last 500 entries. Entries are also persisted to ~/.opentabs/audit.log as NDJSON with automatic rotation at 10 MB (keeping audit.log and audit.log.1). Use opentabs audit --file to query the full disk-based log, which survives server restarts.
curl -s http://127.0.0.1:9515/audit \
-H "Authorization: Bearer <secret>" | jqQuery parameters:
| Parameter | Default | Description |
|---|---|---|
limit | 50 | Maximum number of entries to return (1–500). |
plugin | — | Filter by plugin name (e.g., slack, browser). |
tool | — | Filter by prefixed tool name (e.g., slack_send_message). |
success | — | Filter by result: true for successful invocations, false for failures. |
Each entry contains:
{
"timestamp": "2026-02-21T10:30:00.000Z",
"tool": "slack_send_message",
"plugin": "slack",
"success": true,
"durationMs": 245
}Failed entries include an error object with code, message, and optional category.
/reload and /extension/reload
Both require Bearer token authentication and are rate-limited to 10 requests/minute.
/reload triggers config reload and plugin rediscovery. It is available in both production and dev mode. The opentabs-plugin build command calls this endpoint automatically after each build.
curl -X POST http://127.0.0.1:9515/reload \
-H "Authorization: Bearer <secret>"Response:
{ "ok": true, "plugins": 3, "durationMs": 45 }/extension/reload signals the Chrome extension to reload. Returns 503 if the extension is not connected.
curl -X POST http://127.0.0.1:9515/extension/reload \
-H "Authorization: Bearer <secret>"Authentication
All authenticated endpoints use the same secret from ~/.opentabs/extension/auth.json.
| Transport | Method |
|---|---|
| HTTP endpoints | Authorization: Bearer <secret> header |
WebSocket (/ws) | Sec-WebSocket-Protocol: opentabs, <secret> header |
/health | Optional — unauthenticated returns minimal status; authenticated returns full payload |
When no secret is configured, authentication checks are skipped.
CORS
The server rejects all HTTP requests with a browser Origin header, except those from chrome-extension:// origins. MCP clients (Claude Code, Cursor) run as native processes and don't send Origin headers. This prevents DNS rebinding attacks on the localhost server.
Rate Limiting
Admin endpoints (/reload, /extension/reload) are rate-limited to 10 requests/minute. MCP session creation (/mcp initialize requests) is rate-limited to 5 per minute. Exceeding the limit returns HTTP 429 with Retry-After: 60.
WebSocket Protocol
Server → Extension
| Method | Type | Description |
|---|---|---|
sync.full | Notification | Full plugin list — sent on connection and after reload. |
plugin.update | Notification | Single plugin updated (file watcher detected changes). |
plugin.uninstall | Request | Plugin removed — extension cleans up the adapter and responds to confirm. |
plugins.changed | Notification | Plugin registry changed (after install, update, or remove) — side panel refreshes its plugin list. |
tool.dispatch | Request | Dispatch a tool call to the correct tab's adapter. |
tool.invocationStart | Notification | Tool execution started (side panel animation). |
tool.invocationEnd | Notification | Tool execution completed (side panel animation). |
confirmation.request | Notification | Request user approval for a tool call — includes id, tool name, plugin name, and parameters. |
extension.reload | Notification | Signal the extension to reload (triggered by POST /extension/reload). |
pong | Notification | Reply to keepalive ping. |
Extension → Server
| Method | Type | Description |
|---|---|---|
tab.syncAll | Notification | Full tab-to-plugin state mapping — sent on connection. |
tab.stateChanged | Request | Plugin tab state changed (closed/unavailable/ready). |
tool.progress | Notification | Progress update from a running tool (dispatchId, progress, total, message). |
plugin.log | Notification | Batched plugin log entries from adapter runtime. |
config.getState | Request | Fetch current config state (used by side panel). |
config.setToolPermission | Request | Set permission for a single tool (plugin name + tool name + permission: off/ask/auto). |
config.setPluginPermission | Request | Set default permission for all tools in a plugin (plugin name + permission: off/ask/auto). |
config.setSkipPermissions | Request | Toggle global skip-permissions flag (boolean). |
plugin.search | Request | Search the npm registry for plugins matching a query. |
plugin.install | Request | Install a plugin from npm. |
plugin.updateFromRegistry | Request | Update an installed plugin to the latest registry version. |
plugin.remove | Request | Remove an installed plugin. |
plugin.checkUpdates | Request | Check for available updates to installed plugins. |
plugin.removeBySpecifier | Request | Remove a plugin by npm specifier (package name). |
folder.open | Request | Open a folder path on the host filesystem. |
confirmation.response | Notification | User's approval decision — includes confirmation id, decision ( |
ping | Notification | Keepalive ping. |
Tool Dispatch
- Server validates input against the tool's JSON Schema (pre-compiled with Ajv)
- Server sends a
tool.dispatchrequest over WebSocket - Extension finds the best matching tab by URL pattern and ranking
- Extension executes
handle()viachrome.scripting.executeScriptin MAIN world - Result flows back: Extension → WebSocket → MCP Server → MCP Client
Timeouts: 30 seconds by default, extended by progress reports (up to 5 minutes maximum). Up to 5 concurrent tool calls per plugin; additional calls return an error asking the client to wait for in-flight requests to complete.
MCP Client Configuration
Claude Code
The easiest way is the claude mcp add CLI command:
claude mcp add --transport http opentabs http://127.0.0.1:9515/mcp \
--header "Authorization: Bearer <secret>"Or add manually to ~/.claude.json (merge into the existing "mcpServers" object):
{
"mcpServers": {
"opentabs": {
"type": "streamable-http",
"url": "http://127.0.0.1:9515/mcp",
"headers": {
"Authorization": "Bearer <secret>"
}
}
}
}OpenCode
Add to opencode.json in your project root:
{
"mcp": {
"opentabs": {
"type": "remote",
"url": "http://127.0.0.1:9515/mcp",
"headers": {
"Authorization": "Bearer <secret>"
}
}
}
}Cursor
Add to .cursor/mcp.json:
{
"mcpServers": {
"opentabs": {
"type": "http",
"url": "http://127.0.0.1:9515/mcp",
"headers": {
"Authorization": "Bearer <secret>"
}
}
}
}Windsurf
Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"opentabs": {
"serverUrl": "http://127.0.0.1:9515/mcp",
"headers": {
"Authorization": "Bearer <secret>"
}
}
}
}Replace <secret> with the value from your config:
opentabs config show --json --show-secret | jq -r .secretLast Updated: 10 Mar, 2026