Skip to main content

Logging & Debugging

I built this logging pipeline because I kept flying blind. A tool would fail, and I'd have no idea where in the chain it went wrong — was it the API call? The DOM query? The adapter injection? So I wired up structured logging that flows from your plugin code all the way to the MCP server, and built a set of CLI tools to poke at the system when things aren't working. Here's how I use them.

The log API

The plugin SDK exports a log object with four log levels. These are the only thing standing between you and blindly guessing what your plugin is doing:

import { log } from '@opentabs-dev/plugin-sdk';
 
log.debug('Checking auth state...');
log.info('Fetching channels', { workspace: 'acme' });
log.warn('Rate limit approaching', { remaining: 5 });
log.error('Failed to parse response', { status: 500 });

Each method takes a message string and any number of additional arguments. I made the serialization forgiving on purpose — circular references, DOM nodes, functions, and other non-JSON-safe values are all handled automatically. You shouldn't have to worry about your logging call crashing your tool.

Log Levels

MethodMCP LevelWhen to Use
log.debug()debugDetailed trace information — useful during development, noisy in production
log.info()infoKey operations — what the tool is doing, milestones, results
log.warn()warningSomething unexpected that didn't cause a failure — rate limits, fallback behavior
log.error()errorSomething failed — caught exceptions, missing data, broken assumptions

Where Logs End Up

When your plugin runs inside the browser, logs flow through the entire platform. It's more hops than you'd expect, but each one exists because of the browser's security model:

  1. Tool handler calls log.info(...) in the page context
  2. Adapter IIFE batches entries (flushes every 100ms or 50 entries) and sends them to the Chrome extension
  3. Chrome extension forwards entries to the MCP server via WebSocket
  4. MCP server stores entries in a per-plugin ring buffer (last 1,000 entries), writes them to ~/.opentabs/server.log, and forwards them to connected MCP clients

When running outside the adapter runtime (e.g., in unit tests), log falls back to console methods, prefixed with [sdk.log].

Logging in a Tool Handler

Trust me on this: add logging at key points in your tool handler. Every time I've skipped this step, I've regretted it when something failed and I had no trace of what happened.

import { log, fetchJSON, waitForSelector, ToolError } from '@opentabs-dev/plugin-sdk';
 
async function handle({ query }: { query: string }) {
  log.info('Starting search', { query });
 
  await waitForSelector('.search-loaded');
  log.debug('Search page is ready');
 
  const results = await fetchJSON<{ items: { id: string; title: string }[] }>(`/api/search?q=${encodeURIComponent(query)}`);
  if (!results) throw ToolError.internal('No results returned');
  log.info('Search complete', { resultCount: results.items.length });
 
  if (results.items.length === 0) {
    log.warn('No results found', { query });
  }
 
  return { items: results.items };
}

Debugging Workflow

Here's the order I work through problems when something breaks. Most issues reveal themselves by step 2 — but when they don't, each step gives you a deeper level of visibility.

Step 1: Check System Health with opentabs status

Start here. Nine times out of ten, the thing you think is a plugin bug is actually the server not running or the extension not connected:

opentabs status
OpenTabs Status

  Status:      running
  Version:     0.0.3
  Port:        9515
  Uptime:      2h 15m
  Extension:   connected
  MCP clients: 1
  Plugins:     2
  Tools:       8
  Reloads:     0

  Plugins
    Slack (local) — ready · 5 tools
    GitHub (npm) — unavailable · 3 tools

Look for:

  • Extension: not connected — the Chrome extension isn't connected to the server. Reload the extension from chrome://extensions/
  • Plugins: 0 — no plugins discovered. Run opentabs doctor to check plugin paths
  • unavailable tab state — the plugin's tab exists but isReady() returned false. Open the app in Chrome and log in
  • closed tab state — no matching tab open. Open the web app the plugin targets

Use --json for the raw health response, including logBufferSize per plugin and auditSummary stats.

Step 2: Check Recent Tool Calls with opentabs audit

This is usually where the answer is. The audit log shows you exactly what the AI agent called and whether it worked:

opentabs audit
Time       Tool                      OK   Duration
10:15:30   slack_list_channels    120ms
10:15:45   slack_send_message    30.0s
10:16:02   slack_list_channels    95ms

A red means the tool call failed. Filter by plugin to focus on what matters:

opentabs audit --plugin slack
opentabs audit --limit 50
opentabs audit --json          # raw JSON with error details

The --json output is where the real detail lives — error codes, categories, and messages that tell you exactly what went wrong:

{
  "timestamp": "2026-02-21T10:15:45.000Z",
  "tool": "slack_send_message",
  "plugin": "slack",
  "success": false,
  "durationMs": 30000,
  "error": {
    "code": "TIMEOUT",
    "message": "Dispatch timed out after 30000ms",
    "category": "timeout"
  }
}

Step 3: Read Plugin Logs with opentabs logs

If the audit tells you what failed, the logs tell you why. The server log captures everything — your plugin's log entries, server events, errors:

opentabs logs

This prints the last 50 lines of ~/.opentabs/server.log and exits. Filter to a specific plugin:

opentabs logs --plugin slack

This filters to lines matching [plugin:slack]. Plugin log entries appear in this format:

[plugin:slack] 2026-02-21T10:15:30.000Z INFO Fetching channels {"workspace":"acme"}
[plugin:slack] 2026-02-21T10:15:45.000Z ERROR Request timed out {"url":"/api/chat.postMessage"}

Other useful flags:

opentabs logs --lines 100      # show last 100 lines instead of 50
opentabs logs --follow         # continuously tail new output
opentabs logs -f               # shorthand for --follow

Step 4: Run Diagnostics with opentabs doctor

If things still don't add up, doctor runs a full system diagnostic. I built this for the "everything looks fine but nothing works" situations:

opentabs doctor
OpenTabs Doctor

  ✓ Runtime: Node.js v22.11.0
  ✓ Config file: /Users/you/.opentabs/config.json
  ✓ MCP server: running on port 9515
  ✓ Extension connection: connected
  ✓ Extension installed: /Users/you/.opentabs/extension
  ✓ Extension version: matches (0.0.3)
  ! Plugin: /Users/you/plugins/slack — missing dist/tools.json
    Run: cd /Users/you/plugins/slack && npm run build

6/7 checks passed.

Doctor checks:

  • Runtime — is Node.js 22+ installed?
  • Config file — does ~/.opentabs/config.json exist?
  • MCP server — is the server running and reachable?
  • Extension connection — is the Chrome extension connected via WebSocket?
  • Extension installed — are the extension files in ~/.opentabs/extension/?
  • Extension version — does the extension version match the CLI version?
  • Local plugins — does each plugin in localPlugins have built artifacts?

Step 5: Inspect the Chrome Extension

If you've gotten this far without finding the problem, it's probably in the extension layer — adapter injection, WebSocket connection, or content script relay. Time to open Chrome DevTools:

Service worker logs:

  1. Open chrome://extensions/
  2. Find the OpenTabs extension
  3. Click "Inspect views: service worker"
  4. Check the Console tab for errors

Content script and adapter logs:

  1. Open the web app your plugin targets
  2. Open DevTools (F12)
  3. Check the Console tab — adapter logs and [sdk.log] fallback entries appear here

Side panel:

  1. Open the OpenTabs side panel
  2. Right-click inside it → "Inspect"
  3. Check Console and Network tabs

Common Issues

These are the problems I see most often. If you're stuck, there's a good chance it's one of these.

Tool call times out (30 seconds)

The dispatch chain has a 30-second timeout. If your tool takes longer than that, it's not broken — it just needs to report progress to keep the connection alive:

async function handle(params: Input, context?: ToolHandlerContext) {
  for (let i = 0; i < pages.length; i++) {
    context?.reportProgress({ progress: i + 1, total: pages.length });
    await processPage(pages[i]);
  }
  return { done: true };
}

Each progress report resets the 30-second timer. See the Streaming & Progress guide for details.

Plugin shows "unavailable" state

This one trips people up. The tab is there, but isReady() returned false. Almost always one of two things:

  • The user isn't logged in — open the app and sign in
  • The page hasn't finished loading — isReady() checks for a DOM element that appears after authentication

Check your isReady() implementation:

async isReady(): Promise<boolean> {
  return !!document.querySelector('.authenticated-container');
}

Plugin shows "closed" state

No tab matching the plugin's URL patterns is open. Open the web app in Chrome.

"Not connected" in side panel

The Chrome extension can't reach the MCP server. This is usually simpler than it looks:

  1. Is the server running? → opentabs status
  2. Is it on the expected port? → Default is 9515
  3. Try reloading the extension from chrome://extensions/

Logs aren't appearing

This is the most frustrating one — you added logging, but nothing shows up. Work through these in order:

  1. Confirm the server is running: opentabs status
  2. Confirm the extension is connected: look for "Extension: connected" in status output
  3. Check the browser console for errors in the content script relay
  4. Make sure you're filtering correctly: opentabs logs --plugin <name> matches the plugin name, not the display name

Best Practices

These come from debugging a lot of plugins. Every one of these is a lesson I learned the hard way.

Log at boundaries. Add log.info() at the start and end of tool handlers, and log.debug() before significant operations (API calls, DOM queries). When something fails at 2am, these are the breadcrumbs that tell you what happened.

Include context in log data. Pass objects with relevant IDs, counts, and parameters — not just messages. A log entry that says "Sending message" is useless. One that says { channel: '#general', length: 142 } tells you exactly what happened.

// Good — includes actionable context
log.info('Sending message', { channel: '#general', length: text.length });
 
// Less useful — no context
log.info('Sending message');

Use log.warn() for recoverable issues. If your tool retries an operation, falls back to a different strategy, or encounters something unexpected but non-fatal, log a warning. I've caught rate limit patterns and auth drift early because warnings showed up before things actually broke.

Don't log sensitive data. Avoid logging auth tokens, passwords, or personal data. The log pipeline stores entries in memory and writes them to disk — anything you log lives in ~/.opentabs/server.log.

Next Steps

Good logging gets you halfway to a diagnosis. The rest is about giving the AI agent enough information to handle failures intelligently:

  • Error Handling — I'd read this next. When logging tells you what happened, structured errors tell the AI agent what to do about it
  • Streaming & Progress — if your tools are timing out, progress reporting is probably the fix
  • Plugin Development — the full walkthrough if you're building a plugin from scratch
  • SDK Reference: Utilities — the complete API reference for log and every other SDK utility

Last Updated: 10 Mar, 2026