PbScope MCP Server
Officialby pwrdrvr
README.md
# PbScope
macOS pasteboard diagnostics. PbScope watches `NSPasteboard.general`, records
every clipboard generation as an event (who wrote it, which flavor UTIs were
declared, what they weigh once materialized), and exposes the whole history to
AI agents over an HTTP MCP server — so you can run one instance on the machine
that copies and another on the machine that pastes, and let an agent compare
what each side saw.
Built by [PwrDrvr](https://github.com/pwrdrvr) to diagnose clipboard-transfer
problems in [PwrSnap](https://github.com/pwrdrvr/PwrSnap) (uncompressed-TIFF
size bloat over Universal Clipboard / Splashtop, flavors dying between copy and
paste), but it is PwrSnap-agnostic: it observes the system pasteboard.
## Why observing the clipboard is tricky (and how PbScope handles it)
Many pasteboard flavors are **promised**, not rendered — the source app only
produces the bytes when a paste target asks. Reading a flavor is therefore a
side-effectful act: it resolves promises and can trigger Universal Clipboard
network transfers, changing the very behavior you're debugging.
PbScope is **passive by default**: it polls `changeCount` (~150ms) and
enumerates each item's declared UTIs, which does *not* materialize data. Sizes,
hashes, and previews appear only when you:
- click a flavor chip (fetch that one flavor),
- enable **Auto-fetch contents** (materialize every flavor of each new event),
- run **Paste test** (read every flavor the way a real paste target would), or
- call the side-effect-marked MCP tools.
Every fetch is recorded on the event, so you can tell afterward whether
observation perturbed the experiment.
### Attribution
`NSPasteboard` does not record which app wrote it. PbScope badges events using
presence-only markers in the declared flavor set:
- **PwrSnap** — any `com.pwrdrvr.*` UTI present
- **Universal Clipboard** — `com.apple.is-remote-clipboard`
- **concealed / transient** — the `org.nspasteboard.*` conventions
## Install / run
Requires macOS, Node ≥ 24 (`.nvmrc`), pnpm, and Xcode command line tools
(`swiftc`).
```bash
pnpm install # also compiles the Swift helper (build/native/pasteboard-monitor)
pnpm dev # run the app
```
Package a distributable zip (ad-hoc signed; right-click → Open on the target
machine, or `xattr -dr com.apple.quarantine PbScope.app`):
```bash
pnpm package
```
Closing the window quits the app (and its MCP server) — the server only runs
while PbScope is visibly running in the Dock.
## MCP server
Streamable HTTP, stateless, **no auth** — intended for localhost and trusted
private networks only.
- Default: `http://127.0.0.1:4577/mcp`
- Health check: `GET /healthz`
- LAN mode (for the remote-machine workflow): launch with `--lan` or
`PBSCOPE_HOST=0.0.0.0`; change the port with `PBSCOPE_PORT`.
Connect from Claude Code:
```bash
claude mcp add --transport http pbscope-local http://127.0.0.1:4577/mcp
```
```bash
claude mcp add --transport http pbscope-remote http://192.168.1.50:4577/mcp
```
### Tools
| Tool | Side effects | What it does |
|---|---|---|
| `pb_status` | none | Helper liveness, current changeCount, event count, auto-fetch state |
| `pb_list_events` | none | Event history: declared UTIs per item, markers, superseded flag, recorded fetches (`sinceSeq`, `limit`, `pwrsnapOnly`) |
| `pb_get_event` | none | Full detail for one event by `seq` |
| `pb_fetch_flavor` | **materializes** | `data(forType:)` for one flavor of the current pasteboard → byteLength, sha256, duration (optional inline base64) |
| `pb_paste_test` | **materializes** | Read every declared flavor like a paste target; per-flavor size/duration plus `NSImage(pasteboard:)` decodability |
### A typical two-machine session
1. Run PbScope on the source Mac (`pnpm dev`) and on the paste-target Mac
(`--lan`).
2. Point your agent at both MCP endpoints.
3. Copy an image in the app under test on the source Mac.
4. Ask the agent to `pb_list_events` on both sides and compare: did one copy
produce one event or two? Which flavors were declared on each side? Then
`pb_fetch_flavor` / `pb_paste_test` on the target to see what the paste
actually weighs (a 250 KB PNG arriving as an 8 MB `public.tiff` is your
uncompressed-TIFF bloat) and whether any flavor returns nil.
## Architecture
```
native/pasteboard-monitor/main.swift Swift CLI: polls changeCount, NDJSON
over stdio; fetch/pasteTest on demand
src/main/monitor.ts spawns + speaks to the helper
src/main/event-store.ts ring buffer of events (payloads never stored)
src/main/mcp-server.ts Streamable HTTP MCP (stateless, no auth)
src/main/index.ts Electron bootstrap + IPC
src/renderer/ event timeline UI
```
The Swift helper exists because Electron's clipboard API can't do any of the
load-bearing parts: it exposes no `changeCount`, can't enumerate declared UTIs
without reading them, and can't distinguish promised from rendered flavors.
## License
MIT © PwrDrvr LLC
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues