Saphan StudioDocs
MCP server

MCP worked examples

Real commands and the engine's own messages, verbatim.

Real commands and the engine's own messages, verbatim.

Start, foreground

saphan server start
server: listening on http://127.0.0.1:7654 — MCP: POST /mcp · contract v4 · read-only
install on your machines: 127.0.0.1 is loopback — reachable only from this machine, not the
rest of the fleet.
  for a fleet-reachable bind: saphan server start --listen <lan-ip>:7654 --detach --workspace <ws>

The banner is keyed to the address actually bound, never a hostname guess — and a loopback bind says plainly that its install line is useless to other machines, with the ready alternative.

Widening the bind must name the host:

saphan server start --listen :7654
flag --listen ":7654" leaves the host implicit — that form binds EVERY interface;
widening beyond loopback must be a deliberate act
→ run: saphan server start --listen 127.0.0.1:7654 (loopback), or --listen 0.0.0.0:7654
  to bind all interfaces EXPLICITLY

Local MCP client over stdio

saphan server start --stdio

No listener, no pidfile — the client owns the process lifetime, the operator scope is unrestricted (local trust), paths are unredacted, and the access log goes to stderr because stdout is the wire.

Registering in Claude Code (HTTP variant, against a running listener):

claude mcp add --transport http saphan http://127.0.0.1:7654/mcp

Detached, and reached from outside for an evaluation

⚠ This is the evaluation path, not a production exposure — see Transports for what a quick tunnel does not give you.

saphan server start --detach
server: started (pid 48211, detached) — logs at <log dir>/server.log

The confirmation prints only after the readiness handshake — never a fork-and-lie.

saphan server start --tunnel
server: --tunnel: cloudflared launched, watch stderr above for the assigned hostname

Every route the tunnel forwards is still bearer-gated; the self-issued /authorize and /token refuse over the forwarded hop — mint credentials locally or use an external IdP.

Discovery and the 401 challenge

curl -s http://127.0.0.1:7654/.well-known/oauth-protected-resource/mcp

returns the RFC 9728 metadata: the canonical resource, the authorization server(s), and scopes_supported: stream-state, cost-data, offline_access. An unauthenticated call to the resource:

curl -si -X POST http://127.0.0.1:7654/mcp -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="http://127.0.0.1:7654/.well-known/oauth-protected-resource/mcp"

{"error":{"class":"access-denied","ref":"<opaque>"}}

The challenge names exactly one resource_metadata, and that URL answers 200 — the two properties clients demonstrably break on.

Reading the fleet, within budget

With a valid bearer token:

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
  "name":"fleet_show",
  "arguments":{"state":"in-flight","detail":"summary","limit":25}}}

A defaulted-horizon response carries horizon_note naming the narrowing and the escape hatch (horizon:"all"); a truncated response carries next_after for the follow-up call and keeps isError: false. Ask for too much and the refusal names the fix:

result-too-large: fleet_show response is 214511 bytes, exceeds the 200000-byte cap —
narrow with a state/streams filter, lower limit below its current value, or omit
detail:full (summary, the default, is far smaller)

The read-only wall, in the wire's own words

{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"gate_accept","arguments":{}}}
unknown tool "gate_accept" — this read-only projection serves exactly: fleet_show, wave_show,
ledger_show, board_export, stream_detail, cost_show, capture_show, capture_list,
spend_window, executor_census, fleet_usage_projection (mutating tools do not exist)

Lifecycle

saphan server status
server: running (pid 48211) — listen 127.0.0.1:7654 (default)

saphan server reload
server: reload signal sent (pid 48211)
# → "server: config re-read — state was never held; every request reads the record fresh"

saphan server stop
server: stop signal sent (pid 48211)

On this page