Skip to main content
Glama

🐦 Pigeon

Forward browser console errors straight to Claude Code — no more copy‑pasting stack traces. Pigeon captures console.error/console.warn, uncaught exceptions, unhandled promise rejections, and failed network requests (fetch/XHR) from your dev pages and exposes them to Claude Code over the Model Context Protocol (MCP). Minified stack traces are resolved back to original source via your dev server's source maps.

Browser → Extension → WebSocket ┐
                                 ▼
                         Bridge daemon  ──control channel──►  MCP proxy (per session) → Claude Code
                         (one, shared)                        MCP proxy (per session) → Claude Code

A browser extension can't talk to a CLI process directly, hence the chain. A single bridge daemon owns the WebSocket the extension connects to plus the error buffer; it's started automatically and shared by every session. Each Claude Code session runs a thin MCP proxy that forwards tool calls to the daemon over a local control channel — so multiple sessions and projects can use Pigeon at once (scope each to its dev server with get_recent_errors({ pageUrl })).

Layout

pigeon/
  extension/   # Chrome Manifest V3 extension (TypeScript, esbuild)
  server/      # Bridge daemon (WebSocket + buffer) + per-session MCP proxy (TypeScript)
  README.md

Related MCP server: chrome-devtools-mcp-fork

What Claude gets

Tools

Tool

Purpose

get_recent_errors({ limit?, level?, pageUrl?, since? })

Buffered errors, newest first; filter by level (error/warn/network), pageUrl substring, or since timestamp.

wait_for_next_error({ timeout_ms? })

Block until the next error arrives — “reproduce in the browser, then check.”

get_error_stats()

Counts per level + newest/oldest timestamps.

clear_errors()

Empty the buffer.

get_error_history({ limit?, level?, since? })

When PIGEON_DB is set. Query the on-disk history — spans restarts, beyond the 200-entry buffer.

reload_tab({ tabId? })

Reload a dev tab (active localhost tab by default) — re-trigger an error after a fix.

eval_in_page({ expression, tabId?, timeout_ms? })

Gated. Run JS in the page's MAIN world and return the result. Off unless explicitly enabled (see below).

Prompts (appear as slash-commands in Claude Code's / menu)

Prompt

Purpose

analyze_browser_errors({ limit?, level?, pageUrl? })

Embeds the recent errors and asks for root‑cause analysis + concrete fixes.

fix_latest_error()

Focuses on the single newest error (with its resolved stack) and proposes a fix.

Resources

Resource

Contents

pigeon://errors

Live JSON snapshot of the buffer.

pigeon://errors/{id}/screenshot

JPEG of the page when an uncaught error fired.

pigeon://errors/{id}/dom

outerHTML of the page at error time.

On uncaught errors / unhandled rejections, Pigeon also captures a screenshot (rate-limited, best-effort — the error's tab must be the visible one) and a DOM snapshot. Entries gain hasScreenshot/hasDom flags plus screenshotUri/domUri. Toggle this off in the popup ("Snapshots on errors"); console/network events never carry snapshots.

Each captured error carries level, message, stack, source, line, col, pageUrl, origin, timestamp, plus tabId/tabTitle (which tab it came from) and, for network events, status. When a source map is available, a resolvedStack field is added with the original file:line:col frames.

The server keeps a ring buffer of the latest 200 errors and deduplicates identical message+stack pairs seen within 2 seconds (collapsed into one entry with a count).


Setup

1. Build & start the bridge server

cd pigeon
npm run install:all      # installs server/ and extension/ deps
npm run build            # builds both

Start the bridge. Normally Claude Code launches it for you (see step 3) — but you can run it standalone for testing:

npm start                # = node server/dist/index.js

The bridge listens on ws://127.0.0.1:8765 for the extension and speaks MCP over stdio. All logs go to stderr (or $PIGEON_LOG_FILE) — never stdout, which is reserved for the MCP JSON‑RPC protocol.

Smoke‑test without the extension:

npm test                 # spawns the server, pushes a fake error, asserts the MCP tools
# or, against an already-running server:
npm --prefix server run test:client

2. Load the extension in Chrome

  1. Open chrome://extensions.

  2. Enable Developer mode (top right).

  3. Click Load unpacked and select pigeon/extension/dist.

  4. The 🐦 icon appears. Click it: the popup shows connection status, the number of buffered errors, and three toggles — forwarding on/off, Snapshots on errors, and Allow remote eval ⚠️ (for eval_in_page, off by default).

The extension only activates on http://localhost/* and http://127.0.0.1/* (your dev servers). It connects to the bridge automatically; if the bridge isn't running yet, it reconnects with exponential backoff and buffers errors in the meantime.

Rebuild after changes with npm run dev:extension (watch) or npm --prefix extension run build, then hit Reload on the extension card.

3. Register the MCP server with Claude Code

Use the absolute path to the built entry point. From the pigeon/ directory:

claude mcp add pigeon -- node "$(pwd)/server/dist/index.js"

Add -s user to register it globally (available in every project, not just this one) — recommended if you do web dev across several repos:

claude mcp add -s user pigeon -- node "$(pwd)/server/dist/index.js"

Or add it to a .mcp.json in your project:

{
  "mcpServers": {
    "pigeon": {
      "command": "node",
      "args": ["/Users/martin/claude/pigeon/server/dist/index.js"]
    }
  }
}

To enable the optional features, pass the env vars at registration. With the CLI:

claude mcp add pigeon \
  --env PIGEON_DB="$HOME/.pigeon/history.jsonl" \
  --env PIGEON_ALLOW_EVAL=1 \
  -- node "$(pwd)/server/dist/index.js"

…or in .mcp.json, add an "env" block alongside command/args:

"env": { "PIGEON_DB": "/abs/path/history.jsonl", "PIGEON_ALLOW_EVAL": "1" }

Then, inside Claude Code, verify with /mcp — you should see pigeon connected with its tools and the pigeon://errors resource. (How many tools depends on gating: the four buffer tools plus reload_tab are always there; get_error_history appears with PIGEON_DB set and eval_in_page with PIGEON_ALLOW_EVAL=1.)


Using Pigeon in your Claude Code workflow

Once registered (ideally -s user, so it's there in every project) and the extension shows Connected, run your dev server on localhost and work as usual. Verify with /mcp (pigeon connected) and the popup's green dot.

Recipes

  • Reactive — “what's broken?”“What errors are in the browser right now?” Claude calls get_recent_errors and reads the message + source-mapped stack (original file, not minified).

  • Repro-driven — the core loop. “I'll reproduce it, wait for the error.” Claude calls wait_for_next_error; you trigger it in the browser; it streams in and Claude fixes it. After the fix: “reload the tab”reload_tab → wait again. That's edit → reload → verify without leaving the CLI.

  • Slash-commands. The prompts appear in the / menu: /analyze_browser_errors (group all current errors by root cause) and /fix_latest_error (focus the newest one).

  • Visual / state bugs. Uncaught errors carry a screenshot + DOM snapshot — “look at the screenshot from the last error” (pigeon://errors/{id}/screenshot and /dom).

  • Inspect live (opt-in). With eval enabled (see Browser control below): “evaluate window.__store.getState() in the page”eval_in_page.

  • Recurring errors. With PIGEON_DB set: “has this error happened before?”get_error_history (spans restarts).

Make it smoother

Let Claude reach for Pigeon on its own — add to a project's CLAUDE.md:

## Debugging
For runtime errors in the browser, use the `pigeon` MCP tools
(`get_recent_errors`, `wait_for_next_error`) instead of asking me to paste console output.

Skip permission prompts for the read-only tools — in settings.json:

{ "permissions": { "allow": [
  "mcp__pigeon__get_recent_errors",
  "mcp__pigeon__get_error_stats",
  "mcp__pigeon__wait_for_next_error",
  "mcp__pigeon__get_error_history"
] } }

Leave reload_tab, eval_in_page, and clear_errors to prompt.

Multiple sessions & projects

This works out of the box. A single bridge daemon is auto-started on first use and shared by every Claude Code session — open as many as you like across different projects. It picks free ports automatically (no conflict even if 8765/8766 are taken) and advertises them via ~/.pigeon/runtime.json. All localhost tabs feed the same buffer; scope each session to its own dev server with the pageUrl filter.

What "scope with pageUrl" means. Because every project's tabs feed one shared buffer, you narrow a session to its own errors by filtering on the page URL — a case-insensitive substring match against each error's URL. You don't type the { pageUrl: … } syntax; you just ask in plain language and Claude sets the filter:

“What errors are coming from viacamp?” / “Show the errors from localhost:3000.” → Claude calls get_recent_errors({ pageUrl: "3000" })

Notes:

  • The filter value is whatever is in the tab's address bar that uniquely identifies it — usually the port ("3000", "5173") or "localhost:3000".

  • If only one project is running, you don't need the filter at all — the buffer only holds that project's errors.

  • Drop the proactive snippet from Make it smoother into each project's CLAUDE.md and the session scopes itself automatically.

The daemon keeps running in the background after sessions close (it owns the browser feed) — normally just leave it. To stop it: pkill -f dist/bridge.js. The first session's env (PIGEON_DB, PIGEON_ALLOW_EVAL) configures the daemon, so set those consistently in your user-scope registration; later sessions reuse the already-running daemon as-is.

Dashboard & CLI

The daemon comes with two ways to see and manage what's going on across all your sessions.

pigeon CLI

A small terminal tool for the shared daemon (no browser needed):

node server/dist/cli.js status      # or: npm --prefix server run cli -- status

Command

Shows / does

pigeon status

Daemon health (pid, version, uptime), ports, whether the extension is connected, eval/history flags, buffer counts, and the connected Claude Code sessions — each with its project name and the dev-server it's scoped to.

pigeon doctor

PASS/WARN/FAIL checks with actionable hints (Node version, build present, daemon up, extension connected, ports reachable, eval gating). The first thing to run when something feels off.

pigeon stop

Shut the daemon down gracefully (sessions just respawn it on next use).

pigeon dashboard

Open the web dashboard in your browser.

pigeon status when nothing is running tells you so (it never spawns a daemon). Tip: register the bin once with npm --prefix server link (or add server/dist/cli.js to your PATH) to type pigeon directly.

Web dashboard

The daemon serves a live dashboard on http://127.0.0.1:8767 (first free port from there; see it in pigeon status or the daemon log). It shows daemon health + ports, the connected sessions (which project, since when, which dev-server they're scoped to), and an error feed (level-coloured, click to expand the source-mapped stack + screenshot). The feed has two tabs: Live (the in-memory buffer) and History (the persisted JSONL — only when PIGEON_DB is set; it outlives restarts and the buffer). Buttons clear the buffer or stop the daemon.

The dashboard never pollutes its own data: its page marks itself so the extension skips capturing it (otherwise, running on localhost, it would log its own API traffic into the buffer it displays).

It's on by default; disable with PIGEON_DASHBOARD=0. Security: it binds 127.0.0.1 only, validates the Host header (anti-DNS-rebinding), and gates every API call on a per-daemon token kept in the mode-600 ~/.pigeon/runtime.json — a random web page can't read the token, so it can't drive the dashboard. It shows the eval-gating state but never toggles it (that stays the explicit double opt-in below).

Browser control & security

Pigeon can also drive the browser, so Claude can reproduce a bug rather than only read it:

  • reload_tab — always available; reloads the target tab.

  • eval_in_page — runs arbitrary JavaScript in the page. This is powerful and dangerous, so it is off by default behind a double opt-in — both must be true:

    1. Start the server with PIGEON_ALLOW_EVAL=1 (otherwise the tool isn't even exposed).

    2. Turn on "Allow remote eval ⚠️" in the extension popup (otherwise the extension refuses every eval command).

    It only targets localhost/127.0.0.1 tabs. Leave both off unless you actively want Claude to execute code in your dev page.

Configuration

Env var

Default

Where

Meaning

PIGEON_WS_PORT

8765

daemon

Base extension WebSocket port. The daemon binds the first free port from here; the extension scans the next 16.

PIGEON_CONTROL_PORT

8766

daemon

Base control-channel port (first free from here). Proxies discover the actual port via the runtime file.

PIGEON_DASHBOARD_PORT

8767

daemon

Base web-dashboard HTTP port (first free from here).

PIGEON_DASHBOARD

1 (on)

daemon

Set to 0 to disable the local web dashboard entirely.

PIGEON_LOG_FILE

both

If set, mirror stderr logs to this file (handy to see daemon logs).

PIGEON_RUNTIME_DIR

~/.pigeon

both

Where the daemon writes its discovery file (runtime.json) + lock.

PIGEON_SOURCEMAPS

1

server

Set to 0 to disable source-map resolution of stacks.

PIGEON_ALLOW_EVAL

server

Set to 1 to expose the eval_in_page tool (also needs the popup toggle).

PIGEON_DB

server

Path to a JSONL file; enables persistent history + the get_error_history tool.

Ports are chosen automatically: the daemon takes the first free port at/after each base and records it in ~/.pigeon/runtime.json; proxies read that file and the extension scans the range — so Pigeon runs out of the box even if 8765/8766 are already taken. You only need PIGEON_WS_PORT if you want a different base (then also bump WS_BASE in extension/src/background.ts, since the extension scans from there).

What's captured

  • Console: console.error / console.warn (wrapped, then passed through unchanged). The hooks are installed by a MAIN-world content script at document_start, so they run before any page script and catch even synchronous errors during initial load.

  • Uncaught exceptions (window error) and unhandled promise rejections.

  • Failed network requests: fetch and XMLHttpRequest responses with status ≥ 400 or a transport failure (status 0). Intentional aborts are ignored. Original semantics are preserved — Pigeon never swallows a response or rejection.

Notes & limits

  • Source maps are fetched from the dev server on demand and cached briefly (5 s, so hot-reloads stay accurate). Resolution is best-effort: no map → the raw stack is kept.

  • Only localhost / 127.0.0.1 are matched, by design (your dev servers).

  • The MV3 service worker sleeps after ~30s idle; Pigeon reconnects on wake (incoming messages and a 30s alarms heartbeat) and persists the pending queue in chrome.storage.session, so errors aren't lost across an eviction.

  • One browser, one bridge: the buffer is shared across all matched tabs (filter with get_recent_errors({ pageUrl })).

  • History: with PIGEON_DB=/path/to/history.jsonl, new errors are appended as JSONL and reloaded on startup. The file is append-only and excludes screenshots/DOM (those stay in-memory only) — rotate or delete it yourself.

  • Lighthouse / performance metrics remain out of scope for now.

Development

# server
npm --prefix server run dev             # tsc --watch
npm --prefix server run test:e2e        # full MCP proxy/daemon smoke test
npm --prefix server run test:multi      # two sessions sharing one daemon + pageUrl scoping + session identity
npm --prefix server run test:ports      # daemon shifts ports when 8765/8766 are taken
npm --prefix server run test:dashboard  # dashboard HTTP API: token gating, /api/state, clear
npm --prefix server run test:sourcemap  # source-map resolution test

# extension
npm --prefix extension run dev           # esbuild --watch
npm --prefix extension run typecheck     # tsc --noEmit
npm --prefix extension run test:unit     # pure serialization unit tests
npm --prefix extension run test:browser  # real-browser smoke test (loads the extension)

test:browser loads the built extension into Chrome for Testing (system Google Chrome blocks unpacked extensions). Install it once:

cd extension && node node_modules/playwright-core/cli.js install chromium

CI (.github/workflows/ci.yml) runs everything on each push: a build-test job (build, typecheck, unit + source-map + MCP E2E + multi-session + port-fallback + dashboard) and a browser-e2e job (real-browser smoke test). See CLAUDE.md for the architecture and the invariants worth not regressing.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pepperonas/pigeon'

If you have feedback or need assistance with the MCP directory API, please join our Discord server