Skip to main content
Glama
README.md
# ๐Ÿ™ octoport

**See every MCP tool call your AI agents make โ€” and exactly where they fail.**

A local-first debugger, flight recorder, and control layer for MCP.

```sh
npx octoport up --open
```

Your agents call MCP tools all day. When something breaks โ€” an expired OAuth
token, a crashing server, a tool that silently changed its schema โ€” you get a
vague client error and zero visibility. octoport sits on the path of every MCP
call and turns that darkness into a live, diagnosable record:

```text
15:04:12  Claude Code โ†’ github / create_pull_request    WRITE ยท MEDIUM            โœ“ 381ms
15:04:18  Claude Code โ†’ supabase / execute_sql          DATABASE_WRITE ยท MEDIUM   โœ• 4.2s   AUTH ยท token_expired
15:04:30  Cursor      โ†’ playwright / browser_click      UNKNOWN ยท LOW             โš  12.7s
```

- **Live Activity** โ€” every call, live: client, identity, server, tool, latency,
  risk badge, and a canonical failure diagnosis. The home screen.
- **Failure diagnosis** โ€” every failed call is classified deterministically into
  a canonical taxonomy (`AUTH ยท token_expired`, `NETWORK ยท dns`,
  `SERVER ยท circuit_open`, โ€ฆ) instead of a raw stack trace.
- **Reliability** โ€” per server and per tool: success rate, p50/p95/p99, timeout
  and auth-failure counts, top failure categories, and deterministic anomaly
  alerts (consecutive failures, restart loops, latency spikes).
- **`octoport doctor`** โ€” "why won't this MCP setup work?" answered with
  evidence and a concrete fix, in the CLI and the dashboard (same engine).
- **Tool surface integrity** โ€” every server's tool catalog is snapshotted and
  diffed. A server that quietly grows `delete_repository` triggers a
  risk-classified warning you must **Accept** โ€” or **Block** the tool for every
  client with one click.
- **Local-first privacy** โ€” everything stays on your machine in SQLite.
  Secret-looking argument keys are redacted before they ever hit disk, and
  metadata-only mode (`logPayloads: false`) never stores args/results at all.
  The dashboard always states which mode is active.

## Quickstart โ€” one command, then it's all web

```sh
npx octoport up --open
```

The dashboard walks you through the rest:

1. **We found your MCP servers** โ€” octoport scans your Claude Code / Claude
   Desktop / Cursor / Windsurf / VS Code configs (global *and* project-scoped).
2. **Connect your AI client** โ€” copy one snippet per client.
3. **Make a test call** โ€” and watch it appear in Live Activity, live.
4. Optionally **clean up** the now-duplicate direct entries (per-file preview,
   timestamped backups, restorable from Settings).

From then on your clients talk to one endpoint, and octoport sees everything:
add/edit/toggle servers centrally, and new servers propagate to every connected
client live (`tools/list_changed`), no restarts.

> Until the npm package is published, run from source:
> `git clone https://github.com/nullarch/octoport && cd octoport && npm install && npm run build && node dist/cli.js up --open`

Start at login:

```sh
octoport autostart          # launchd (macOS) / systemd --user (Linux) / schtasks (Windows)
octoport autostart --remove
```

Port 6286 is "OCTO" on a phone keypad. Change it with `octoport up --port <n>`.

## When MCP breaks

```text
$ octoport doctor

SERVER
  โœ• github: error โ€” AUTH ยท token_expired
      The credential has expired โ€” re-authenticate.
      raw: Error POSTing to endpoint (HTTP 401): Unauthorized
      fix: Reauthorize "github": revoke the stored grant (Servers โ†’ github โ†’ Grants)
           and authorize again, or refresh the credential in its headers/env.
```

Doctor checks the environment (Node, config permissions), your clients
(detected configs, whether they point at the hub, whether a session ever
connected), every server (status, classified failure, stderr tail, last
successful call), and the gateway (tokens, privacy mode, policy, IdP
reachability). `--json` for scripts; the dashboard's Doctor tab runs the same
engine.

## One endpoint for everything

Under the observability layer, octoport is a full MCP gateway โ€” that's *why*
it sees every call:

- **One endpoint** โ€” N clients ร— M servers becomes 1 ร— M.
- **Process supervision** โ€” stdio servers run as child processes with crash
  isolation, jittered backoff restart, start/call timeouts, a circuit breaker
  after 8 straight failures, and a per-server stderr ring buffer.
- **Hosted servers with OAuth** โ€” add a hosted MCP URL; if it wants OAuth the
  server card shows **Authorize** (PKCE, one-time state, dynamic client
  registration). Tokens live in your config dir, `0600`.
- **Full catalog aggregation** โ€” tools, prompts (`server__prompt`), and
  resources (URIs pass through, reads routed to the owning server). The call
  log records all three kinds.
- **Server-initiated requests relayed** โ€” `sampling`/`elicitation`/`roots`
  answered by the client whose call is executing on that server.
- **Named access tokens** โ€” one per person or machine; revoking kills live
  sessions on the spot. Scopes: `admin` (dashboard) vs `mcp` (endpoint).
- **Local security by default** โ€” `127.0.0.1` binding, Host/Origin validation
  (DNS-rebinding protection), per-install bearer token. The MCP Inspector RCE
  (CVE-2025-49596) happened because a localhost tool skipped exactly these.
- **Blocked tools** โ€” a per-server denylist (globs) enforced at `tools/list`
  *and* on every call; the "Block" button on surface-change warnings writes it.

## Remote access (optional, explicit)

octoport never exposes itself beyond loopback unless you say so, twice.

**Recommended: Tailscale.** Keep the default loopback bind, or bind the
tailnet address; nothing touches the public internet:

```sh
octoport up --host 100.x.y.z --allow-host hub.tailnet-name.ts.net --behind-proxy
```

**Reverse proxy (Caddy) with a real domain.** octoport does not terminate TLS
โ€” your proxy does:

```
# Caddyfile
hub.example.com {
  reverse_proxy 127.0.0.1:6286
}
```

```sh
octoport up --allow-host hub.example.com --behind-proxy --public-url https://hub.example.com
```

`--public-url` makes snippets, the dashboard, and OAuth callbacks use the
external address; the dashboard WebSocket switches to `wss://` on HTTPS.
`GET /healthz` answers without a token for proxy health checks. A non-loopback
bind refuses to start without `--allow-host` **and** `--behind-proxy`;
`X-Forwarded-Host` is deliberately ignored.

**Small team setup:** run octoport on a shared box behind Tailscale, one named
token per teammate, and Live Activity becomes a team-level "which agent did
what" record.

## Organization mode (experimental)

> **Status: functional, not yet pilot-verified.** The organization gateway is
> feature-complete for a first pilot and covered by the test suite (including
> user-boundary attack repros), but has not yet been run end-to-end against a
> real IdP and real MCP clients in production. Treat it as **alpha**. Personal
> mode is stable.

One MCP endpoint for every employee: IT registers servers once, employees add
one URL and sign in with **company SSO**, access follows group **profiles**,
per-user services are authorized individually, and every call is centrally
audited under a verified identity.

**Company SSO (generic OIDC).**

```jsonc
// config.json
"oidc": {
  "issuer": "https://login.example.com/realms/acme",
  "audience": "octoport",                  // aud your IdP mints for this hub
  "groupsClaim": "groups",                 // default
  "adminGroups": ["platform-admins"]       // these groups may open the dashboard
}
```

Tokens validate against the IdP's JWKS (discovery, cache, rotation; RS256/384/
512 + ES256/384; iss/aud/exp/nbf enforced). Unauthenticated requests get a
`WWW-Authenticate` challenge pointing at
`/.well-known/oauth-protected-resource` (RFC 9728). Add `loginClientId` for a
browser **Sign in with SSO** flow (`/login`, Authorization Code + PKCE).
Disable the account at the IdP and access dies โ€” octoport has no user database
of its own, on purpose.

**Profiles (group-based access).** Once any profile exists the MCP surface is
default-deny. Enforced at `tools/list` *and* on every call; denials are logged.
Settings has a policy **simulator** ("what would this identity see?").

```jsonc
"profiles": {
  "backend-engineer": {
    "groups": ["engineering"],
    "servers": ["github", "jira", "sentry"],
    "denyTools": ["kubernetes__delete_*"]  // globs on public names; deny wins
  },
  "ci": { "tokens": ["github-actions"], "servers": ["github"] }
}
```

**Per-user OAuth downstream.** `"credentialMode": "per_user_oauth"` gives
every identity its own connection and OAuth grant โ€” employee A's GitHub is
never employee B's GitHub. Employees self-serve at **`/my`**; admins can
revoke any grant (kills its live connection). Secrets can be references
(`file://โ€ฆ`, `vault://โ€ฆ`) instead of literals.

**Central audit.** Every row records the verified subject, including policy
denials. Admin actions get their own event trail (`GET /api/events`, Audit
tab); `GET /metrics` serves Prometheus.

Deployment: `Dockerfile` + [`deploy/docker-compose.yml`](deploy/docker-compose.yml)
(+ a [Keycloak demo](deploy/docker-compose.keycloak.yml)); guides in
[docs/deploy.md](docs/deploy.md) and [docs/idp.md](docs/idp.md); design in
[docs/threat-model.md](docs/threat-model.md).

## Commands

| Command | What it does |
|---|---|
| `octoport up [--open] [--port <n>] [--host <addr>] [--allow-host <h>] [--behind-proxy] [--public-url <url>]` | Start the hub |
| `octoport doctor [server] [--json]` | Diagnose client/server/gateway problems, with fixes |
| `octoport autostart [--remove]` | Start at login (launchd/systemd) |
| `octoport import [--dry-run]` | CLI import (the wizard does this too) |
| `octoport connect [client]` | Print connection snippets |
| `octoport logs [--limit <n>] [--jsonl [file]]` | Inspect or export the call log |
| `octoport token [--rotate]` | List tokens / rotate the default token (live rotation: dashboard โ†’ Rotate) |

## Data & privacy principles

1. Local-first by default โ€” SQLite on your machine, `0600`.
2. Metadata-only is first-class: `logPayloads: false` stores no args/results.
3. Secrets are redacted before persistence, in every mode.
4. The dashboard always states exactly what is stored.
5. Catalog snapshots store hashes, not tool descriptions/schemas.

## What octoport is not

- **Not a finished enterprise governance product.** SSO, profiles, per-user
  OAuth, and verified audit exist (see *Organization mode*), but there's no
  SCIM, no tenant separation, and no compliance claims yet.
- **Not agent mind-reading.** octoport sees transport truth โ€” what was called,
  what failed, what changed. It does not claim to know *why* a model chose a
  tool; that requires client-side context it doesn't collect today.
- **Not a context optimizer for Claude Code.** Claude Code ships native MCP
  tool search; octoport doesn't compete with it.
- **Not magic.** Tool names are namespaced `server__tool` (capped at 48 chars
  with deterministic hashing) and routed through a mapping table โ€” never
  string-parsed back.

## Known limitations (v0.10)

- Server-initiated relay (sampling/elicitation/roots) on a **shared** server
  uses in-flight correlation, and refuses when two identities call it at once
  (mark such servers `per_user_oauth` to use those features safely).
- Catalog snapshots/diffs cover shared servers; per-user servers can
  legitimately show different surfaces per identity and are not snapshotted.
- Config edits outside the dashboard need a restart (dashboard changes apply
  hot); rotate a token live from Settings โ†’ Rotate.
- `autostart` requires a permanent install (`npm i -g octoport` or a repo
  checkout) โ€” refuses to point launchd at an ephemeral npx cache.
- Personal-mode tokens are full-admin by default; org mode uses scoped
  (`admin`/`mcp`) tokens, per-user OAuth, and profiles.

## Support expectations

MIT-licensed, provided **as is** โ€” a solo-maintained project, so no
response-time promises. Issues and PRs are welcome and read; small, focused
PRs with tests have the best odds. If you need guarantees, fork freely.

## Development

```sh
npm install
npm test          # 207 tests: unit + E2E incl. mock-OAuth, mock-OIDC, mock-Vault
npm run build
node dist/cli.js up --open
```

The telemetry/event schema is documented in [docs/events.md](docs/events.md).
Product direction and priorities live in [PIVOT.md](PIVOT.md) (it supersedes
[ROADMAP.md](ROADMAP.md) for ordering); the organization-gateway details stay
in ROADMAP.md.