Operations
Day-2 reference: the endpoints a running gateway serves, every metric and audit field it emits, what the error codes mean to client teams, and how to observe reloads and discovery. Deployment shapes (Docker, Helm, systemd, TLS, Redis) are in /deployment/; the configuration document is in /configuration/.
HTTP endpoints
Section titled “HTTP endpoints”| Endpoint | When | Notes |
|---|---|---|
POST/GET /mcp | always | The MCP endpoint (path configurable via server.mcpPath). |
GET /health | always | Pings every upstream concurrently (5 s internal budget); 503 when none is reachable. The fan-out is single-flighted and reused for a second, so polling this unauthenticated endpoint in a loop can’t multiply into upstream traffic (a reload or discovery sync invalidates it immediately). /healthz — the path through v1.4, kept as a deprecated alias through v1.8 — was removed in v1.9; a probe left on it now 404s. Detailed fields (URLs, owners, labels, error text) appear only when auth is disabled, so an unauthenticated caller on a public deployment cannot enumerate the federation. Multi-endpoint upstreams include a per-replica endpoints array with the balancer’s rotation state. |
GET /metrics | always | Prometheus exposition (below). |
GET /api/federation | server.introspection.enabled | The federation snapshot — upstream health and topology, each upstream’s source (static vs discovered) and credential-strategy name, discovery status, policy shape, audit sink types, shared-state and tracing facts, and the viewer’s tenant governance. Authenticates like /mcp and shares its rate budgets. Was GET /console/api/state through v1.8. |
GET /api/auth-hint | server.introspection.enabled | The deliberately unauthenticated sign-in hint — issuer, client id, scopes, resource — read by a browser client before it holds a token. Public SPA configuration only. Was GET /console/api/auth through v1.8. |
GET /console/ | server.console.enabled | The read-only console page. Its assets are open (they carry no data); the dashboard reads /api/federation like any other client of it, so the page also requires server.introspection.enabled. |
GET /.well-known/oauth-protected-resource | auth.mode: required | RFC 9728 resource metadata; announces the EMA extension when configured. |
GET /.well-known/jwks.json | EMA configured | fold’s minting key. |
POST /oauth/token | EMA configured | The ID-JAG exchange endpoint — unauthenticated by design (the assertion is the credential) and rate limited (auth.ema.tokenRateLimitPerMinute). |
Every endpoint sits behind the allowedHosts check — health checkers and scrapers must send an allowed Host header (see /deployment/#allowedhosts-and-health-probes).
Quick checks against a running gateway:
curl -fsS http://localhost:8080/healthcurl -fsS http://localhost:8080/metricsserver.mcpPath defaults to /mcp; server.maxBodyBytes defaults to 1 MiB (larger request bodies, including chunked ones cut off at the cap, are answered 413).
Metrics
Section titled “Metrics”| Metric | Labels | Meaning |
|---|---|---|
fold_requests_total | method, outcome | MCP requests through the gateway. Outcomes mirror audit: ok, error, denied, rate_limited, upstream_down. |
fold_request_duration_seconds | method | End-to-end request duration histogram. |
fold_upstream_requests_total | upstream, outcome | Proxied upstream calls. Outcomes: ok, rate_limited, circuit_open, connect_error, error. A JSON-RPC error from the upstream counts as ok — the upstream answered; its error passes through verbatim. |
fold_upstream_request_duration_seconds | upstream | Upstream call duration histogram. |
fold_upstream_breaker_state | upstream | 0 closed, 1 half-open, 2 open. |
fold_upstream_endpoint_healthy | upstream, endpoint | Multi-endpoint upstreams only: 1 in rotation, 0 ejected after a connect failure (or by an active health probe). |
fold_http_rejections_total | reason | Requests refused before the MCP layer: body_too_large, forbidden_host, forbidden_origin, unauthenticated, rate_limited, oauth_token_rate_limited. |
fold_discovery_syncs_total | outcome | Discovery polls: applied, unchanged, rejected (document failed parse or merged validation), error (fetch failed). |
fold_request_upstream_calls | — | Histogram of the upstream fan-out per request — the unit budgets are charged in, which makes the cost of a tools/list visible against a ping. |
fold_budget_degraded_total | — | Budget checks that could not reach shared state and fell back to per-instance enforcement. See below. |
fold_build_info | version | Always 1. |
With tenants configured, two more carry the tenant dimension:
| Metric | Labels | Meaning |
|---|---|---|
fold_tenant_requests_total | tenant, outcome | Requests by the tenant’s principals. |
fold_tenant_upstream_calls_total | tenant | Upstream invocations charged to the tenant — an allowance being spent, watchable. |
These are separate series rather than a tenant label on the metrics above, because label sets are frozen by the compatibility contract and a new label would break every dashboard built on them.
Plus the standard Go process/runtime collectors. Alerting starters: fold_upstream_breaker_state == 2 sustained, any fold_http_rejections_total rate spike, and — with discovery — any rejected/error sync outcomes. See /discovery/ for what a discovery sync does.
Alert on any non-zero fold_budget_degraded_total rate. A budget check that cannot reach shared state degrades to per-instance enforcement rather than to none, which means a fleet is no longer enforcing one allowance — three instances become three copies of it. The gateway also warns at startup when a budget is configured without server.redisUrl. See /consumption/.
Audit events
Section titled “Audit events”One JSON event per terminal response — including 401s, 403-equivalents, and 429s — to the configured sinks (stdout, webhook; delivery is asynchronous and batched, never adding request latency). Fields:
| Field | Meaning |
|---|---|
time | UTC timestamp. |
principal, issuer | Verified subject and token issuer; absent when auth is disabled. |
method | MCP method (tools/call, …) or http for pre-MCP rejections. |
name | Namespaced tool/prompt name or resource URI. |
upstream | Routed upstream id. |
decision, ruleId | Policy outcome (allow/deny) and the matching rule. |
outcome | ok, error, denied, rate_limited, unauthenticated, upstream_down, forbidden, budget_exhausted. |
error | Error text, when the request failed. |
latencyMs | End-to-end latency. |
tenant | The principal’s tenant, when one is configured and resolves. Present on denials too. |
upstreamCalls | Upstream invocations this request caused — the unit budgets are charged in. |
itemsServed | For list methods: what was handed to this caller, after policy filtering. |
usage | Carried verbatim from an upstream’s result _meta. fold reports it, it does not compute it — there is no tokenizer in the gateway. |
Example event on the stdout sink:
{"time":"2026-08-04T15:03:22Z","principal":"user@acme.com","issuer":"https://acme.okta.com","method":"tools/call","name":"gh__list_issues","upstream":"github-tools","decision":"allow","ruleId":"gh-readers","outcome":"ok","latencyMs":42}Sink configuration and shipping the stdout stream to a SIEM are covered in /deployment/#audit-and-logs.
Gateway error codes
Section titled “Gateway error codes”What client teams see when the gateway itself refuses a request (upstream errors pass through verbatim):
| Code | Meaning | Client action |
|---|---|---|
-32040 | Per-upstream rate limit exceeded | Back off (message includes retry hint). |
-32041 | Upstream unavailable (circuit open / unreachable / all down) | Retry later; transient. |
-32042 | Policy denied the invocation | Not transient — the principal lacks a grant. |
-32043 | Name resolves to no configured namespace | Refetch the tool list. |
-32044 | Consumption budget exhausted for the period | Not transient — the message names the reset instant, not a retry delay. Distinct from -32040 because a rate limit clears in seconds and a budget does not clear until the period rolls. |
-32002 | Task id not owned by any upstream | The task is unknown or belongs to another principal. |
-32602 | Invalid or expired list cursor | Restart the list from the beginning. |
HTTP-level refusals: 401 (missing/invalid token, with a WWW-Authenticate challenge), 403 (host/origin not allowed), 413 (body over server.maxBodyBytes), 429 (+ Retry-After).
Observing reloads and discovery
Section titled “Observing reloads and discovery”Reloads (SIGHUP, --watch, Reload) log configuration reloaded with the upstream, discovered, and retired counts — or an error naming what was rejected (reload: the auth section cannot change without a restart, validation failures) while the old configuration keeps serving. Clients receive list_changed after every successful swap. See /deployment/#hot-reload for the three ways a reload is triggered per deployment shape.
Discovery logs state transitions rather than every poll: discovery applied with the upstream count, discovery fetch failed once per outage (with discovery source recovered on the way back), and discovery document rejected/malformed once per bad document. The fold_discovery_syncs_total outcomes carry the per-poll record. See /discovery/ for how upstreams are discovered and merged.
Active health probes (healthCheck.intervalMs) log only transitions: health probe ejected endpoint and health probe restored endpoint.
Tracing
Section titled “Tracing”W3C trace context propagates to upstream calls unconditionally. With the tracing section configured, fold also emits its own spans over OTLP/HTTP: a server span per MCP request named by method, carrying mcp.method, mcp.name, fold.upstream, fold.outcome, fold.policy.decision, fold.policy.rule, and enduser.id (the same terminal fields as the audit event), and a client span per upstream call (upstream <id>) closed with its guard outcome. Export is batched off the request path; Close/shutdown flushes with a 3 s bound so a dead collector cannot hang termination.
Log streams
Section titled “Log streams”Operational logs go to stderr via log/slog (--log-format text|json, --log-level debug|info|warn|error). Per-request accounting deliberately stays out of the log stream — that is what metrics and the audit sinks are for. Startup, upstream connect failures and session drops, breaker transitions, refused cross-host redirects, reload results, discovery and probe transitions, and shutdown are the events to expect at info/warn (successful upstream connects log at debug).
For deployment-shape specifics — Docker, Kubernetes/Helm, VM/systemd, TLS fronting, and Redis for fleets — see /deployment/.