Skip to content

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/.

EndpointWhenNotes
POST/GET /mcpalwaysThe MCP endpoint (path configurable via server.mcpPath).
GET /healthalwaysPings 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 /metricsalwaysPrometheus exposition (below).
GET /api/federationserver.introspection.enabledThe 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-hintserver.introspection.enabledThe 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.enabledThe 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-resourceauth.mode: requiredRFC 9728 resource metadata; announces the EMA extension when configured.
GET /.well-known/jwks.jsonEMA configuredfold’s minting key.
POST /oauth/tokenEMA configuredThe 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:

Terminal window
curl -fsS http://localhost:8080/health
curl -fsS http://localhost:8080/metrics

server.mcpPath defaults to /mcp; server.maxBodyBytes defaults to 1 MiB (larger request bodies, including chunked ones cut off at the cap, are answered 413).

MetricLabelsMeaning
fold_requests_totalmethod, outcomeMCP requests through the gateway. Outcomes mirror audit: ok, error, denied, rate_limited, upstream_down.
fold_request_duration_secondsmethodEnd-to-end request duration histogram.
fold_upstream_requests_totalupstream, outcomeProxied 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_secondsupstreamUpstream call duration histogram.
fold_upstream_breaker_stateupstream0 closed, 1 half-open, 2 open.
fold_upstream_endpoint_healthyupstream, endpointMulti-endpoint upstreams only: 1 in rotation, 0 ejected after a connect failure (or by an active health probe).
fold_http_rejections_totalreasonRequests refused before the MCP layer: body_too_large, forbidden_host, forbidden_origin, unauthenticated, rate_limited, oauth_token_rate_limited.
fold_discovery_syncs_totaloutcomeDiscovery 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_infoversionAlways 1.

With tenants configured, two more carry the tenant dimension:

MetricLabelsMeaning
fold_tenant_requests_totaltenant, outcomeRequests by the tenant’s principals.
fold_tenant_upstream_calls_totaltenantUpstream 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/.

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:

FieldMeaning
timeUTC timestamp.
principal, issuerVerified subject and token issuer; absent when auth is disabled.
methodMCP method (tools/call, …) or http for pre-MCP rejections.
nameNamespaced tool/prompt name or resource URI.
upstreamRouted upstream id.
decision, ruleIdPolicy outcome (allow/deny) and the matching rule.
outcomeok, error, denied, rate_limited, unauthenticated, upstream_down, forbidden, budget_exhausted.
errorError text, when the request failed.
latencyMsEnd-to-end latency.
tenantThe principal’s tenant, when one is configured and resolves. Present on denials too.
upstreamCallsUpstream invocations this request caused — the unit budgets are charged in.
itemsServedFor list methods: what was handed to this caller, after policy filtering.
usageCarried 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.

What client teams see when the gateway itself refuses a request (upstream errors pass through verbatim):

CodeMeaningClient action
-32040Per-upstream rate limit exceededBack off (message includes retry hint).
-32041Upstream unavailable (circuit open / unreachable / all down)Retry later; transient.
-32042Policy denied the invocationNot transient — the principal lacks a grant.
-32043Name resolves to no configured namespaceRefetch the tool list.
-32044Consumption budget exhausted for the periodNot 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.
-32002Task id not owned by any upstreamThe task is unknown or belongs to another principal.
-32602Invalid or expired list cursorRestart 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).

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.

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.

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/.