Skip to main content

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.

Connection Modes

OpenTabs supports three ways for AI agents to use its tools — each trading context cost for discoverability.

ModeContext costHow it worksBest for
Full MCPAll enabled tools in contextStandard MCP — connect to /mcp, all tool schemas loaded upfront.Maximum capability, autocomplete-style tool use.
Gateway MCP2 tools in contextConnect to /mcp/gateway — exposes opentabs_list_tools and opentabs_call. AI discovers tools on demand.Many plugins enabled but context window is limited.
CLI modeZero MCP overheadNo MCP connection. AI calls opentabs tool list and opentabs tool call via shell.Agents with Bash access (Claude Code). Minimal context footprint.

Full MCP

The default. Your AI client connects to /mcp and receives every tool from enabled plugins plus built-in browser tools. This is what opentabs start prints config blocks for.

If you only enable a few plugins, the context cost is modest — 5 plugins with 20 tools each is ~100 tools. Disabled plugins (permission off) are still listed but marked as [Disabled] in descriptions.

Gateway MCP

Connect to /mcp/gateway instead of /mcp. The AI sees exactly 2 tools:

  • opentabs_list_tools — discover available tools, optionally filtered by plugin name
  • opentabs_call — invoke any tool by name with arguments

The workflow: call opentabs_list_tools to see what's available, then opentabs_call to use a specific tool. Full capability, minimal context.

{
  "mcpServers": {
    "opentabs": {
      "type": "http",
      "url": "http://127.0.0.1:9515/mcp/gateway",
      "headers": {
        "Authorization": "Bearer <secret>"
      }
    }
  }
}

CLI Mode

No MCP connection at all. Your AI uses the opentabs CLI via Bash:

# Discover tools
opentabs tool list
opentabs tool list --plugin slack
 
# View a tool's input schema
opentabs tool schema slack_send_message
 
# Call a tool
opentabs tool call slack_send_message '{"channel":"C123","text":"hello"}'

The server must be running (opentabs start), but the AI doesn't need an MCP configuration — it just needs Bash access and knowledge that opentabs tool call exists.

For Claude Code, add a line to your project's CLAUDE.md:

Use `opentabs tool list` and `opentabs tool call <name> '<json>'` to interact with web apps via the browser.

HTTP Endpoints

The server runs on port 9515 by default (override with the PORT environment variable or --port flag).

EndpointMethod(s)AuthDescription
/mcpPOST, GET, DELETEBearer tokenMCP Streamable HTTP transport — AI clients connect here.
/mcp/gatewayPOST, GET, DELETEBearer tokenGateway MCP endpoint — 2 meta-tools (opentabs_list_tools, opentabs_call).
/toolsGETBearer tokenREST tool listing — used by opentabs tool list. Supports ?plugin= filter.
/tools/:name/callPOSTBearer tokenREST tool invocation — used by opentabs tool call.
/wsWebSocket upgradeSec-WebSocket-ProtocolChrome extension connection (full-duplex JSON-RPC).
/ws-infoGETBearer tokenReturns WebSocket URL for extension bootstrap.
/healthGETOptional Bearer token

Server health, plugin status, and metrics. Authenticated requests return the full payload.

/auditGETBearer tokenTool invocation history (last 500 entries).
/reloadPOSTBearer token, rate-limitedTrigger config reload and plugin rediscovery.
/extension/reloadPOSTBearer token, rate-limitedSignal the Chrome extension to reload.
/plugin-settingsPOSTBearer token, rate-limitedPlugin settings update — accepts { plugin, settings }, validates against configSchema. Used by opentabs plugin configure.

/mcp — MCP Streamable HTTP

The primary endpoint for MCP clients. Implements the MCP Streamable HTTP transport.

HeaderRequiredDescription
AuthorizationYes (if secret configured)Bearer <secret> — the 64-char hex token from ~/.opentabs/extension/auth.json.
mcp-session-idAfter initializationSession 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/message to connected clients
  • Progress — tool progress notifications forwarded as notifications/progress (requires progressToken in 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 connections
  • sdkVersion — server-level SDK version
  • browserToolCount — number of built-in browser tools registered
  • pluginToolCount — total number of plugin tools across all loaded plugins
  • browserToolNames — 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 tab
  • pluginDetails[].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 buffer
  • failedPlugins — plugins that failed to load (path + error message)
  • disabledBrowserTools — browser tool names that have been disabled via config
  • skipPermissions — whether all permission checks are bypassed (via OPENTABS_DANGEROUSLY_SKIP_PERMISSIONS=1 env var)
  • lastReloadTimestamp — milliseconds since epoch of the last plugin reload (0 if never reloaded)
  • lastReloadDurationMs — how long the last reload took in milliseconds
  • stateSchemaVersion — internal state schema version number
  • discoveryErrors — array of non-fatal errors encountered during plugin discovery
  • fileWatcher — 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, including last24h breakdown and avgDurationMs

/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>" | jq

Query parameters:

ParameterDefaultDescription
limit50Maximum number of entries to return (1–500).
pluginFilter by plugin name (e.g., slack, browser).
toolFilter by prefixed tool name (e.g., slack_send_message).
successFilter 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.

TransportMethod
HTTP endpointsAuthorization: Bearer <secret> header
WebSocket (/ws)Sec-WebSocket-Protocol: opentabs, <secret> header
/healthOptional — 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

MethodTypeDescription
sync.fullNotificationFull plugin list — sent on connection and after reload.
plugin.updateNotificationSingle plugin updated (file watcher detected changes).
plugin.uninstallRequestPlugin removed — extension cleans up the adapter and responds to confirm.
plugins.changedNotificationPlugin registry changed (after install, update, or remove) — side panel refreshes its plugin list.
tool.dispatchRequestDispatch a tool call to the correct tab's adapter.
tool.invocationStartNotificationTool execution started (side panel animation).
tool.invocationEndNotificationTool execution completed (side panel animation).
confirmation.requestNotification

Request user approval for a tool call — includes id, tool name, plugin name, and parameters.

extension.reloadNotificationSignal the extension to reload (triggered by POST /extension/reload).
pongNotificationReply to keepalive ping.

Extension → Server

MethodTypeDescription
tab.syncAllNotificationFull tab-to-plugin state mapping — sent on connection.
tab.stateChangedRequestPlugin tab state changed (closed/unavailable/ready).
tool.progressNotificationProgress update from a running tool (dispatchId, progress, total, message).
plugin.logNotificationBatched plugin log entries from adapter runtime.
config.getStateRequestFetch current config state (used by side panel).
config.setToolPermissionRequestSet permission for a single tool (plugin name + tool name + permission: off/ask/auto).
config.setPluginPermissionRequestSet default permission for all tools in a plugin (plugin name + permission: off/ask/auto).
config.setSkipPermissionsRequestToggle global skip-permissions flag (boolean).
config.setPluginSettingsRequestSet plugin configuration values (plugin name + settings map).
plugin.searchRequestSearch the npm registry for plugins matching a query.
plugin.installRequestInstall a plugin from npm.
plugin.updateFromRegistryRequestUpdate an installed plugin to the latest registry version.
plugin.removeRequestRemove an installed plugin.
plugin.checkUpdatesRequestCheck for available updates to installed plugins.
plugin.removeBySpecifierRequestRemove a plugin by npm specifier (package name).
folder.openRequestOpen a folder path on the host filesystem.
confirmation.responseNotification

User's approval decision — includes confirmation id, decision (allow or deny) and optional alwaysAllow boolean for auto-approving future invocations.

pingNotificationKeepalive ping.

Tool Dispatch

  1. Server validates input against the tool's JSON Schema (pre-compiled with Ajv)
  2. Server sends a tool.dispatch request over WebSocket
  3. Extension finds the best matching tab by URL pattern and ranking
  4. Extension executes handle() via chrome.scripting.executeScript in MAIN world
  5. 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": "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 .secret

Last Updated: 29 Mar, 2026