octoport
by nullarch
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.
This server cannot be deployed
Maintenance
ActivityNo data
ResponsivenessNo issues