COMPASS docs

Connect Claude Code, Cursor, and Codex

The MCP server (mcp/server.py) supports two transports, selected by the COMPASS_MCP_TRANSPORT environment variable on the server process (default stdio). Which one you use changes where the key comes from — get this part wrong and you'll either get a confusing 401 or, with the old behaviour this replaced, silently see another tenant's data.

stdio — Claude Code and Cursor, run locally

The client starts mcp/server.py itself as a subprocess. The key lives in the process environment you give it — there's no separate caller to read a header from, so COMPASS_KEY_RO/COMPASS_KEY_RW are the only source in this mode.

Claude Code (.mcp.json) and Cursor (.cursor/mcp.json) take the same shape:

{
  "mcpServers": {
    "compass": {
      "command": "/path/to/mcp/.venv/bin/python",
      "args": ["/path/to/mcp/server.py"],
      "env": {
        "COMPASS_CORE_URL": "http://localhost:18080",
        "COMPASS_KEY_RO": "<read-only key>",
        "COMPASS_KEY_RW": "<read-write key>"
      }
    }
  }
}
  • COMPASS_CORE_URL — the Core's base URL, without /api/v1 (default http://localhost:18080).
  • COMPASS_KEY_RO — used for read tools; if unset, read tools fall back to COMPASS_KEY_RW.
  • COMPASS_KEY_RW — used for write tools; if unset, a write tool call is refused client-side rather than sent without a key.

Codex

Codex CLI reads MCP servers from ~/.codex/config.toml, as a [mcp_servers.<name>] table with the same command/args/env shape:

[mcp_servers.compass]
command = "/path/to/mcp/.venv/bin/python"
args = ["/path/to/mcp/server.py"]

[mcp_servers.compass.env]
COMPASS_CORE_URL = "http://localhost:18080"
COMPASS_KEY_RO = "<read-only key>"
COMPASS_KEY_RW = "<read-write key>"

streamable-http — a running container

Point at https://<host>/mcp (or http://localhost:18082/mcp locally). This is the mode the deployed MCP runs under, and it accepts two kinds of credential — exactly one per request, never both at once.

The first path: OAuth, no key

A client that speaks OAuth needs no key in its configuration. With Claude Code it is two steps:

claude mcp add --transport http compass https://<host>/mcp

Then, in the chat, /mcp → "Authenticate": the browser opens the consent page, you sign in and grant access. Until then claude mcp list says "Needs authentication" — that is not a failure, it is the state before consent.

The session then belongs to the person who consented, with their role and tenant. If the server has no signing key set for the door (COMPASS_MCP_OAUTH_SIGNING_KEY), the OAuth paths answer 503 and name the missing variable — they do not quietly fall back to open access. The key path is unaffected by this.

The second path: key as a header

Use a key instead of OAuth when the agent is meant to work as a specific persona (its briefing, its limits) or runs without a browser — on a server, say. A key can be bound to a persona; an OAuth session never is, it belongs to the signed-in person.

{
  "mcpServers": {
    "compass": {
      "url": "http://localhost:18082/mcp",
      "transport": "http",
      "headers": { "X-Compass-Key": "<key for this tenant>" }
    }
  }
}

On this path the headers block is required, not optional — a request with neither a key nor an OAuth token does not get through, and one carrying both is refused as an ambiguous credential. When a real incoming HTTP request is present, the server reads the key exclusively from that request's own X-Compass-Key header — it does not fall back to any key baked into the process's own environment, even if one happens to be set. Omit the header and the call is refused before the Core is ever contacted:

{"status": 401, "detail": "X-Compass-Key fehlt in der Anfrage — kein Ruecksprung auf den Prozess-Schluessel."}

This matters because the same key determines both which tenant you see and what you're allowed to do (ro/rw/bruecke — see Concepts) — the same header works for every tool, read or write; the Core decides server-side whether the presented key is enough.

Connecting without a key — what you will see

Clients that speak OAuth need no key in their configuration. They send you to the browser once instead, and you decide there.

What the page tells you:

  • Who is asking. The name the client registered under. If you do not recognise it, decline.
  • Who you are signed in as. Your address, as COMPASS knows it.
  • Which project. If you belong to several, the choice is required and nothing is preselected — otherwise the wrong answer gets clicked through. If you belong to exactly one, you are not asked.
  • What the client may do, in full sentences: read, record, and what is explicitly out of reach — seeing keys, changing members, deleting anything. If your access is read-only, recording is listed under "Not", with the reason.

Then two buttons: Allow or Decline. If you decline, nothing is created and the client stays unconnected.

If something goes wrong, the page says which of these four it was instead of just "error":

What it says What it means
Sign-in expired The window stood open too long. Nothing happened — click "Connect" again in your client.
Wrong project The account you are signed in with has no access to this project. Sign in with another account, or pick a different project above.
Unknown assistant The requesting program could not identify itself. COMPASS will not connect it. If you set it up yourself, use the key route.
COMPASS is not responding Nothing was saved and nothing was connected. Try again.

The page speaks German or English, whichever your browser asks for; German when in doubt.

After connecting, choose the persona in the chat, with persona_waehlen. Only then is it settled who the assistant writes as and what it may see. Without a choice, everything stays as it was.

Which transport for which client

Client Typical setup Transport Key source
Claude Code, local runs server.py itself stdio process env
Cursor, local runs server.py itself stdio process env
Codex, local runs server.py itself stdio process env
Claude Code, against the deployed server claude mcp add --transport http streamable-http OAuth, no key
Any client, against a shared/deployed server connects over HTTP streamable-http per-request header or OAuth

Verify the connection

Once connected, a read tool that doesn't depend on any existing data is the cheapest check:

persona_list()

or, from the shell, the equivalent REST call:

curl -sS "$COMPASS_URL/personas" -H "X-Compass-Key: $COMPASS_KEY"

A 401 here means the key or header is wrong for the transport you're using — recheck the table above before assuming the server itself is down.