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
| Method | MCP Level | When to Use |
|---|---|---|
log.debug() | debug | Detailed trace information — useful during development, noisy in production |
log.info() | info | Key operations — what the tool is doing, milestones, results |
log.warn() | warning | Something unexpected that didn't cause a failure — rate limits, fallback behavior |
log.error() | error | Something 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:
- Tool handler calls
log.info(...)in the page context - Adapter IIFE batches entries (flushes every 100ms or 50 entries) and sends them to the Chrome extension
- Chrome extension forwards entries to the MCP server via WebSocket
- 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 statusOpenTabs 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 doctorto 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 auditTime Tool OK Duration
10:15:30 slack_list_channels ✓ 120ms
10:15:45 slack_send_message ✗ 30.0s
10:16:02 slack_list_channels ✓ 95msA 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 detailsThe --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 logsThis prints the last 50 lines of ~/.opentabs/server.log and exits. Filter to a specific plugin:
opentabs logs --plugin slackThis 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 --followStep 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 doctorOpenTabs 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.jsonexist? - 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
localPluginshave 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:
- Open
chrome://extensions/ - Find the OpenTabs extension
- Click "Inspect views: service worker"
- Check the Console tab for errors
Content script and adapter logs:
- Open the web app your plugin targets
- Open DevTools (F12)
- Check the Console tab — adapter logs and
[sdk.log]fallback entries appear here
Side panel:
- Open the OpenTabs side panel
- Right-click inside it → "Inspect"
- 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:
- Is the server running? →
opentabs status - Is it on the expected port? → Default is
9515 - 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:
- Confirm the server is running:
opentabs status - Confirm the extension is connected: look for "Extension: connected" in status output
- Check the browser console for errors in the content script relay
- 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
logand every other SDK utility
Last Updated: 10 Mar, 2026