Skip to main content
Glama
README.md
# Swarmscope

Swarmscope is a local MCP server and a live graph of your AI agents: who they are, and how they talk to each other.

It runs on your machine. Events sit in a SQLite file under `~/.swarmscope`. There is no account, no cloud, and no telemetry. The viewer is a neon graph on `127.0.0.1`, with live updates, a time scrubber, loop badges, and a stuck-agent ring.

![Swarmscope viewer showing a sample agent network](docs/viewer.png)

## 30-second quickstart

Node.js 22 or newer.

```sh
# Sample network, then the viewer at http://127.0.0.1:4319
npx -y github:josephcake/swarmscope demo
```

Add it to an MCP client and the same command speaks MCP on stdio. It also serves the viewer, so agents can report events while you watch the graph.

```sh
npx -y github:josephcake/swarmscope
```

Record one event from a script:

```sh
npx -y github:josephcake/swarmscope emit \
  --from planner --to coder --kind handoff --label "handoff · draft"
```

Or POST it. HTTP writes need the bearer token created at `~/.swarmscope/token` on first run:

```sh
curl -sS -X POST http://127.0.0.1:4319/api/v1/emit \
  -H 'content-type: application/json' \
  -H "authorization: Bearer $(cat ~/.swarmscope/token)" \
  -d '{"from":"planner","to":"coder","kind":"handoff","label":"handoff · draft"}'
```

`demo` writes `~/.swarmscope/demo.db` and does not touch the real log. `emit` and the MCP server use `~/.swarmscope/events.db`.

## MCP client config

The process speaks [Model Context Protocol](https://modelcontextprotocol.io) over stdio. While it runs, the viewer and a stateless Streamable HTTP endpoint are at `http://127.0.0.1:4319` (`/mcp`). Set `SWARMSCOPE_NO_UI=1` if you want stdio only.

### Claude Desktop

`claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "swarmscope": {
      "command": "npx",
      "args": ["-y", "github:josephcake/swarmscope"]
    }
  }
}
```

### Cursor

`.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "swarmscope": {
      "command": "npx",
      "args": ["-y", "github:josephcake/swarmscope"]
    }
  }
}
```

### VS Code

`.vscode/mcp.json`:

```json
{
  "servers": {
    "swarmscope": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "github:josephcake/swarmscope"]
    }
  }
}
```

### Any stdio client

```json
{
  "command": "npx",
  "args": ["-y", "github:josephcake/swarmscope"]
}
```

### Streamable HTTP

Start `swarmscope ui` (or the default MCP command, which opens the same port) and point a client at:

```json
{ "url": "http://127.0.0.1:4319/mcp" }
```

## Tools

| Tool | What it does |
|---|---|
| `report_event` | One agent reports a message, handoff, review, file write, action, alert, heartbeat, or tool call. |
| `get_network` | Agents, plus edges `{from, to, kind, count, first_ts, last_ts}`. |
| `list_events` | The log, with `since` / `limit` paging and `kind`, `from`, `to` filters. |

`report_event` arguments: `from`, `kind`, and `label` are required. `to` is required for `message`, `handoff`, and `review`. Optional: `status` (`ok` or `error`), `tool`, `tags`, `file`, `parent_id`, `event_id`, `test`. Kinds: `message`, `handoff`, `review`, `file_write`, `action`, `alert`, `heartbeat`.

Sending the same `event_id` again does nothing. Labels are short and neutral: 60 characters or fewer, no URLs, no emails, no secrets.

Ask an agent to call `report_event` when it hands work to another agent. Use slugs you choose (`planner`, `researcher`, `coder`, `reviewer`, …).

## Viewer

`swarmscope ui` or `swarmscope demo` serves the graph on port 4319 (override with `--port` or `SWARMSCOPE_PORT`). It binds to `127.0.0.1` only.

- WebSocket live updates, and polling if the socket drops
- 1h / 6h / 24h / all
- Replay scrubber for the last 24 hours
- Loop badge when two agents hand work back and forth
- Amber ring when a handoff has sat unanswered
- Agent detail panel and an activity feed
- Scroll, pinch, and drag to zoom and pan

The phone layout stacks the feed under the graph. Open an agent and the detail panel sits below the canvas instead of covering it.

![Swarmscope viewer at a phone width](docs/viewer-phone.png)

## Event schema

Full field list, edge model, HTTP paths, and the OTLP mapping: [docs/event-spec.md](docs/event-spec.md).

A handoff looks like this:

```json
{
  "schema_version": "1",
  "event_id": "evt-planner-0001",
  "name": "agent.handoff",
  "start_time_unix_nano": "1750000000000000000",
  "status": "ok",
  "attributes": {
    "gen_ai.agent.id": "planner",
    "gen_ai.agent.name": "Planner",
    "bn.target.id": "coder",
    "gen_ai.tool.name": "apply_edit",
    "bn.label": "handoff · draft",
    "bn.source": "self-report"
  }
}
```

`POST /api/v1/network/events` accepts that object, or `{"events":[...]}` with up to 100. `POST /v1/traces` accepts OTLP/HTTP JSON. Both need the same bearer token as `/api/v1/emit`.

## Security

Binding to `127.0.0.1` is not enough. A page you visit can still reach a loopback port, including by DNS rebinding (the site's name resolves to `127.0.0.1`, so the browser treats it as that site).

- `Host` must be `127.0.0.1`, `localhost`, or `[::1]`, with this process's port. Anything else is `403`.
- A request that sends an `Origin` from anywhere other than those loopback names is `403`. The [MCP Streamable HTTP spec (2026-07-28)](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) requires this check. Requests with no `Origin` (curl, stdio clients) are allowed.
- Writes (`POST /api/v1/emit`, `POST /api/v1/network/events`, `POST /v1/traces`, and `report_event` on `POST /mcp`) require `Content-Type: application/json`. `text/plain` is `415`, so a page cannot use a simple cross-origin POST.
- Those writes also require `Authorization: Bearer` with the token in `~/.swarmscope/token`. The file is created on first run, mode `0600`. Logs and these docs show the path only. Stdio MCP does not use the token. `swarmscope emit` reads the file when it posts to a viewer that is already up.
- The viewer is same-origin and only reads, so opening `http://127.0.0.1:4319` needs no token.

```sh
# refused: wrong host
curl -sS -D - -o /dev/null http://127.0.0.1:4319/api/v1/health -H 'host: evil.example'

# refused: text/plain write
curl -sS -D - -o /dev/null -X POST http://127.0.0.1:4319/api/v1/emit \
  -H 'content-type: text/plain' \
  -H "authorization: Bearer $(cat ~/.swarmscope/token)" \
  -d '{"from":"planner","to":"coder","kind":"message","label":"message · note"}'

# accepted
curl -sS -X POST http://127.0.0.1:4319/api/v1/emit \
  -H 'content-type: application/json' \
  -H "authorization: Bearer $(cat ~/.swarmscope/token)" \
  -d '{"from":"planner","to":"coder","kind":"handoff","label":"handoff · draft"}'
```

## Privacy

All of this stays on the machine it runs on.

- SQLite at `~/.swarmscope/events.db` (or a JSON file if `node:sqlite` is unavailable)
- Write token at `~/.swarmscope/token` (mode `0600`). It is not an account and it is not sent anywhere
- Kept for 7 days (`SWARMSCOPE_RETENTION_DAYS`)
- Bound to `127.0.0.1`
- No accounts, no cloud API keys, no analytics, and no outbound requests
- The viewer ships its own fonts, so the page does not call a font host

Do not put secrets, message bodies, or personal data in labels.

## Migration

Vocabulary revision 1.1. The wire `schema_version` is still `"1"`.

- Send `agent.action` instead of `agent.trade_action`, and `kind: "action"` to `report_event` and `swarmscope emit`.
- Send `bn.tags` (short slugs such as `draft` or `needs-review`) instead of `bn.tickers`. The flat field is `tags`.

Older senders may still use `trade_action` and `tickers`. The server stores them as `agent.action` and `bn.tags`. An old symbol such as `BRK.A` is stored as `brk-a`.

## Commands

```text
swarmscope                  MCP on stdio, viewer on :4319
swarmscope ui               viewer only
swarmscope demo             sample network, then the viewer
swarmscope emit [flags]     write one event
swarmscope --help
```

`--db`, `--home`, and `--port` override the data file, the data directory, and the port.

## Development

```sh
npm install
npm test
npm run build
node dist/cli.js demo
```

Tests use vitest and cover event validation, edge aggregation, both storage backends, the MCP tools, HTTP emit, the websocket, and OTLP ingest. `npm test` also runs `check:dist`, which fails when `dist/` does not match a fresh build. `dist/` is committed so `npx github:josephcake/swarmscope` can run without a compile step. `npm install` rebuilds when the TypeScript toolchain is present.

## License

MIT. See [LICENSE](LICENSE).

TDQS

A4.2/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: report_event writes events, list_events reads raw events with pagination/filters, and get_network reads aggregated agent-edge topology. The descriptions make the boundary between raw events and aggregated network data explicit, so misselection is unlikely.

Naming Consistency5/5

All three tools follow a consistent verb_noun snake_case pattern: get_network, list_events, report_event. No mixed conventions or vague verbs are present.

Tool Count5/5

Three tools cover the minimal complete surface for this domain: ingest, list, and network view. Each tool earns its place, and the small set is easy to scan without redundancy.

Completeness4/5

The core lifecycle is covered (write events, read events, view derived network), but there are minor gaps: no direct event-by-id lookup, no pagination for get_network beyond the 200-edge cap, and no option to include test traffic in network views. Agents can work around most of these using list_events filters, but some dead ends remain.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues