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 /healthzalwaysPings every upstream concurrently (5 s internal budget); 503 when none is reachable. 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 /.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/healthz
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_build_infoversionAlways 1.

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.

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.
errorError text, when the request failed.
latencyMsEnd-to-end latency.

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