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.

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.
/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.

/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).
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": "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 .secret

Last Updated: 10 Mar, 2026