Configuration
fold is configured with a single JSON document, validated on startup (fold --validate). On Node it loads from --config <path> / FOLD_CONFIG (a file path); on Workers from the FOLD_CONFIG var (the JSON itself). The authoritative schema lives in packages/config.
{ "upstreams": [ /* required, at least one */ ], "auth": { /* optional; default: disabled */ }, "policy": { /* optional; absent = allow-all */ }, "audit": { /* optional; absent = no audit emission */ }, "routing": { /* optional */ }, "server": { /* optional */ }}upstreams
Section titled “upstreams”One entry per MCP server folded into the gateway.
| Field | Default | Notes |
|---|---|---|
id | — | Lowercase alphanumeric + hyphens. Used in policy, audit, health. |
url | — | The upstream’s MCP endpoint. |
namespace | none | Tool/prompt name prefix ({namespace}__{name}). Omitted → zero-copy passthrough; only valid with a single upstream. Required when multiple upstreams are configured. |
protocol | auto | "legacy" routes through the held-session bridge for pre-2026 servers. auto/2026-07-28 proxy over stateless HTTP. |
owner | none | { org, team, contact } — which organization runs this upstream. Surfaces in audit and health. |
labels | none | Free-form string map for reporting and future policy selectors. |
auth | {"strategy":"none"} | Upstream credential strategy — see below. |
timeouts | 5s/60s/120s | connectMs, requestMs, streamIdleMs. |
circuitBreaker | 5 / 30s | failureThreshold consecutive failures open the circuit; probes resume after halfOpenAfterMs. |
rateLimit | none | { requestsPerMinute } for this upstream only. |
Upstream auth strategies
Section titled “Upstream auth strategies”| Strategy | Fields | When |
|---|---|---|
none | — | Trusted network, no upstream auth. |
static | secretRef, header?, scheme? | API-key upstreams. secretRef resolves via the runtime’s secret store (env var on Node, Workers secret on the edge). |
passthrough | — | Forwards the client’s Bearer token as-is. The token’s audience is fold, not the upstream — upstreams doing strict RFC 8707 checks will reject it. Prefer token-exchange. |
client-credentials | tokenEndpoint, clientId, clientAuth, scopes?, resource? | Service identity per upstream (SEP-1046). Tokens cached until 60s before expiry. |
token-exchange | tokenEndpoint, clientId, clientAuth, audience, scopes? | RFC 8693 — exchanges the caller’s token for an upstream-audience token, preserving user identity end-to-end. Recommended enterprise default. Cached per (upstream, subject). |
clientAuth: { "type": "client_secret_post" | "client_secret_basic", "secretRef": "..." }.
auth (gateway authentication)
Section titled “auth (gateway authentication)”{ "mode": "required", // "disabled" (default) | "required" "resource": "https://gw.example.com", // canonical resource URI = required token audience (RFC 8707) "issuers": [ { "issuer": "https://acme.okta.com", "jwksUri": "https://acme.okta.com/oauth2/v1/keys", // default: {issuer}/.well-known/jwks.json "mode": "direct", // clients present this issuer's tokens directly "groupsClaim": "groups" // Okta "groups", Entra "roles", Auth0 custom-namespaced } ], "ema": { // optional: Enterprise-Managed Authorization "idpIssuer": "https://acme.okta.com", "idpJwksUri": "...", // optional "signingKeyRef": "FOLD_EMA_KEY", // ES256 private key, PKCS8 PEM, via secret store "tokenTtlSec": 600 }}With mode: "required", every /mcp request needs a valid Bearer token: trusted issuer (checked before any network I/O), verified signature via cached JWKS, exact audience match, RS256/ES256/EdDSA only. Failures answer 401 with a WWW-Authenticate challenge pointing at /.well-known/oauth-protected-resource (RFC 9728).
With ema configured, fold exposes POST /oauth/token accepting exactly one grant — the ID-JAG exchange (urn:ietf:params:oauth:grant-type:jwt-bearer) — and publishes its signing key at /.well-known/jwks.json. The enterprise IdP is policy gate one (it decides who gets an ID-JAG); fold’s policy engine is gate two.
policy
Section titled “policy”{ "defaultDecision": "deny", "rules": [ { "id": "eng-github", "subjects": { "groups": ["engineering"] }, // and/or "subs"; omit → any principal "clients": ["https://claude.ai/.well-known/cimd"], // CIMD URLs; omit → any client "allow": [ { "server": "github", "methods": ["tools/call"], "names": ["get_*", "create_pr"] }, { "server": "search" } // all methods/names on that upstream ] } ]}First matching rule allows; otherwise defaultDecision. Policy governs named invocations (tools/call, prompts/get) and filters list results per principal — callers never see tools they cannot call. Protocol plumbing (ping, discover, the lists themselves) is not policy-gated; invisibility plus call-denial is the enforcement pair.
{ "sinks": [ { "type": "stdout" }, { "type": "webhook", "url": "https://siem.example.com/ingest", "headers": { "x-api-key": "..." } } ] }One event per terminal response — including 401s, 403s, and 429s — with principal, upstream, authz decision + rule id, outcome, and latency. See AuditEvent in packages/contracts.
server
Section titled “server”| Field | Default | Notes |
|---|---|---|
mcpPath | /mcp | Path the gateway serves MCP on. |
allowedHosts | localhost set | DNS-rebinding protection: allowed Host/Origin hostnames. Set to your public hostname(s) in production, or ["*"] only behind a trusted proxy that controls Host. |
rateLimit | none | Global { requestsPerMinute } across all upstreams. |
routing
Section titled “routing”| Field | Default | Notes |
|---|---|---|
namespaceSeparator | __ | Fixed in v0. |
conflictPolicy | prefix | Reserved for future conflict handling. |