Saphan StudioDocs
MCP server

MCP administrator's runbook

Prerequisites, the settings with security meaning, what to monitor, and incident response.

Prerequisites

RequirementDetail
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 workspaceThe server serves one workspace's record; start it where the record lives or pass --workspace.
For --tunnelThe cloudflared binary on PATH. Nothing else — quick tunnels need no account config.
For mode = externalA human-written ISSUERS.md trust list row: issuer, one audience, jwks_uri, alg is one of RS256 or ES256.

Settings with security meaning

SettingValuesDefaultOn a bad value
--listen / SAPHAN_SERVER_LISTEN / server.listenhost:port, host required127.0.0.1:7654Host-less form refused at every tier, class listen-ambiguous, with the tier named
--access-logpath · off · empty (stdout; stderr under --stdio)stdoutUnwritable path refuses the start, class access-log-unwritable
[oauth] modeself-issued · externalself-issuedexternal without issuer is a config refusal
--stdio with --detach or --tunnelRefused: detach-stdio-conflict / tunnel-needs-listener
Trust list (ISSUERS.md)human-gated rowsabsent = self-issued onlyAn unloadable/tampered list is its own named cause (trust list could not be loaded), distinct from "issuer not on it"

What to monitor

SignalWhy it matters
Ledgered auth causesTwelve 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 refusalsA valid token whose actor has no registry mapping — either a provisioning gap or someone probing with a real but foreign token.
access-denied refsCorrelate 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 refusalsThe 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 pathsRefusals 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 signalsUnchanged 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-Authenticate presence, 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_id from 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.

On this page