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 four ways for AI agents to use its tools — each trading context cost for discoverability.
| Mode | Context cost | How it works | Best for |
|---|---|---|---|
| Full MCP | All enabled tools in context | Standard MCP — connect to /mcp, all tool schemas loaded upfront. | Maximum capability, autocomplete-style tool use. |
| Gateway MCP | 2 tools in context | Connect to /mcp/gateway — exposes opentabs_list_tools and opentabs_call. AI discovers tools on demand. | Many plugins enabled but context window is limited. |
| CLI mode | Zero MCP overhead | No MCP connection. AI calls opentabs tool list and opentabs tool call via shell. | Agents with Bash access (Claude Code). Minimal context footprint. |
| Auto-start via stdio | All enabled tools in context | MCP client spawns opentabs start --stdio as a subprocess; bridge relays to running HTTP server. | Users who prefer not to manage a background server process. |
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 nameopentabs_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.
Auto-start via stdio
No manual opentabs start required. Configure your MCP client to spawn opentabs start --stdio directly — the client manages the process lifecycle.
For Claude Code, add to ~/.claude.json:
{
"mcpServers": {
"opentabs": {
"command": "opentabs",
"args": ["start", "--stdio"]
}
}
}The bridge connects to the HTTP server already running on the configured port (default 9515). The HTTP server must be running separately — opentabs start --stdio is a bridge, not a server.
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. |
/mcp/gateway | POST, GET, DELETE | Bearer token | Gateway MCP endpoint — 2 meta-tools (opentabs_list_tools, opentabs_call). |
/tools | GET | Bearer token | REST tool listing — used by opentabs tool list. Supports ?plugin= filter. |
/tools/:name/call | POST | Bearer token | REST tool invocation — used by opentabs tool call. |
/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. |
/plugin-settings | POST | Bearer token, rate-limited | Plugin 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.
| 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)
Auto-injected tool parameters
The platform modifies every plugin tool's input schema at registration time by injecting additional parameters. Plugin authors never declare these — they are transparent to plugin code and stripped from the input before the tool handler runs. The same injected schemas apply uniformly to /mcp, /mcp/gateway (via opentabs_call), and the REST /tools/:name/call endpoint.
| Parameter | Type | Required? | Injected when |
|---|---|---|---|
tabId | integer (≥ 1) | No | Always — present on every plugin tool. When omitted, the platform auto-selects the best matching tab. |
instance | string enum | Yes (when present) | Only when a plugin has 2 or more instances configured. Omitting it causes the call to fail schema validation. |
tabId
Every plugin tool accepts an optional tabId integer. When supplied, the platform dispatches the call to that exact browser tab. When omitted, the platform picks the best available tab automatically.
Use plugin_list_tabs to discover which tabs are open and ready for a plugin, along with their IDs.
instance
When a plugin has two or more instances configured (via a url-type field in its configSchema whose user value is a Record<string, string> map of instance names to URLs), the platform injects instance as a required string enum. The enum values are the user-chosen instance names from config.json.
Example: a user configures a Jira plugin with:
"settings": {
"jira": {
"instanceUrl": {
"work": "https://acme.atlassian.net",
"personal": "https://me.atlassian.net"
}
}
}Every Jira tool's schema will include "instance": { "type": "string", "enum": ["work", "personal"] } in its required array. Calling the tool without instance returns a validation error.
If a plugin has zero or one instance configured, instance is not present on the tool's schema.
See configuration → settings for how to configure url-type settings with multiple instance entries.
Example call with both parameters
{
"name": "jira_list_issues",
"arguments": {
"instance": "work",
"tabId": 42,
"project": "PLAT"
}
}instance selects which site to target; tabId targets a specific browser tab open to that site. Use plugin_list_tabs to retrieve both valid tabId values and the instance label associated with each tab.
/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": 117,
"browserToolCount": 76,
"pluginToolCount": 41,
"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). |
config.setPluginSettings | Request | Set plugin configuration values (plugin name + settings map). |
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 25 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 .secretLast Updated: 24 Apr, 2026