MCP server
MCP administrator's runbook
Prerequisites, the settings with security meaning, what to monitor, and incident response.
Prerequisites
| Requirement | Detail |
|---|---|
| Provisioned identity registry | ≥1 actor, ≥1 binding, loadable root pin — saphan identity import after the ceremony flow in Security §13. The server refuses to start without it. |
| A workspace | The server serves one workspace's record; start it where the record lives or pass --workspace. |
For --tunnel | The cloudflared binary on PATH. Nothing else — quick tunnels need no account config. |
For mode = external | A human-written ISSUERS.md trust list row: issuer, one audience, jwks_uri, alg is one of RS256 or ES256. |
Settings with security meaning
| Setting | Values | Default | On a bad value |
|---|---|---|---|
--listen / SAPHAN_SERVER_LISTEN / server.listen | host:port, host required | 127.0.0.1:7654 | Host-less form refused at every tier, class listen-ambiguous, with the tier named |
--access-log | path · off · empty (stdout; stderr under --stdio) | stdout | Unwritable path refuses the start, class access-log-unwritable |
[oauth] mode | self-issued · external | self-issued | external without issuer is a config refusal |
--stdio with --detach or --tunnel | — | — | Refused: detach-stdio-conflict / tunnel-needs-listener |
Trust list (ISSUERS.md) | human-gated rows | absent = self-issued only | An unloadable/tampered list is its own named cause (trust list could not be loaded), distinct from "issuer not on it" |
What to monitor
| Signal | Why it matters |
|---|---|
| Ledgered auth causes | Twelve named causes. signing key unavailable (JWKS unreachable) is transient (IdP outage, self-heals); signing key could not be resolved from the trust list never self-heals — fix ISSUERS.md. A rising bearer token expired count at a steady ~15-minute cadence is the token lifetime, not an attack. |
token-unmapped refusals | A valid token whose actor has no registry mapping — either a provisioning gap or someone probing with a real but foreign token. |
access-denied refs | Correlate wire-visible refs against the ledger to reconstruct any denial. A burst of scope denials from one tenant is a misconfigured client; from many, a probe. |
result-too-large refusals | The fleet outgrew a client's habits — expected as records grow; the message tells the caller the fix, but a persistent count means a consumer is not reading it. |
| Access-log 4xx on virtual paths | Refusals are logged honestly even when the MCP wire wraps them in 200. This is where you see the surface being used wrongly. |
| Host-key / registry signals | Unchanged from Security §12 — this server adds no new trust events of its own. |
Incident response
- Recurring disconnect + re-auth demands. Run the discriminator first: the same
endpoint from Claude Code CLI and from the cloud-origin client, same time window. CLI
works + cloud fails → the known client-side class; collect the
ofid_reference and report it upstream — do not harden your own server expecting it to fix the other end of the cable. Both fail → capture what actually returns (status,WWW-Authenticatepresence, whether the socket closed) and the event cadence; ~15-minute cadence points at token expiry, ~5-minute at an idle timeout in something in front of you. - "Clearing the client's cache used to fix it." That is a diagnosis, not a ritual: a
cached
client_idfrom dynamic registration going stale when an external realm is reprovisioned. Correlate realm-provisioning timestamps with the cache-clearing moments; if they line up, the case is closed. - Suspected token theft. Tokens live 15 minutes and there is no refresh grant — exposure is bounded by design. For an external issuer, revoke at the IdP; for self-issued, the 15-minute ceiling bounds exposure.
- Suspected leak via the projection. The HTTP surface redacts host roots and the log holds no query strings or values; what a token could read is bounded by its data classes and tenant slice, and every read denial and every ledgered act is keyed for reconstruction.