fold
OfficialProvides access to Cloudflare's MCP documentation server, enabling querying of Cloudflare docs through the gateway.
Provides access to Git repositories via the GitMCP upstream server, exposing version control tools through the gateway.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@foldlist all available tools across all connected MCP servers"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
fold
fold: the enterprise MCP gateway — one governed endpoint between every MCP client and every MCP server.
fold sits in front of any number of upstream MCP servers — in any language, on any SDK, from any team or vendor — providing federation, enterprise auth, policy, caching, rate limiting, and audit. It is built on the official MCP Go SDK, so the wire protocol (streamable HTTP, both request/response and SSE) is the SDK's own implementation on both the client-facing and upstream-facing sides.
Conformant, provably. The official @modelcontextprotocol/conformance suite runs against fold fronting the reference everything-server on every merge — 40/40 checks, including sampling, elicitation, logging, progress, and resource subscriptions bridged through the gateway. The receipt is one click away: the conformance job from the v1.15.0 release run, and every green run on main. A weekly job re-runs the suite against the latest unpinned SDK and opens a tracking issue the moment anything drifts. Reproduce it yourself with ./scripts/conformance.sh — the same command CI runs.
Use cases
Unify a federation. Acquisitions, child orgs, and teams each ship their own MCP servers; fold presents them as one virtual server with namespaced tools — no team rewrites anything.
Draw the security boundary. One choke point for authentication, deny-by-default tool allowlists (once a
policyblock is present — an absent one is allow-all, and the deploy checklist says so), per-principal visibility, and an audit event for every request including denials.Broker credentials. Clients hold one token with the gateway as audience; the gateway exchanges it per upstream (RFC 8693) or injects service credentials — API keys never reach agents.
Protect fragile services. Caching, global and per-upstream rate limits, and circuit breakers stand between agent traffic storms and your internal systems.
Govern vendor MCP servers. Put third-party/SaaS MCP endpoints behind your own auth, policy, and audit instead of scattering per-user API keys.
Related MCP server: Multi-MCP Hub
Quick start
cat > fold.config.json <<'EOF'
{
"upstreams": [
{ "id": "github", "url": "https://mcp.example.com/mcp", "namespace": "github" }
]
}
EOF
go run github.com/fold-run/fold/cmd/fold@latest --config fold.config.json --port 8080
# MCP endpoint: http://localhost:8080/mcp
# Binds 127.0.0.1 by default; pass --host 0.0.0.0 to expose beyond loopback.Or run the container (~22 MB, distroless):
docker run --rm -p 8080:8080 \
-e FOLD_CONFIG="$(cat fold.config.json)" \
ghcr.io/fold-run/fold:latestFOLD_CONFIG accepts either a file path or the JSON document itself (convenient for container env injection). Prebuilt binaries for linux/darwin (amd64/arm64) are on the releases page, or go install github.com/fold-run/fold/cmd/fold@latest.
A single upstream without a namespace runs in passthrough mode (no name rewriting). Multiple upstreams require namespaces; tools and prompts are exposed as {namespace}__{name}.
Embedding in your own Go service is one call:
gw, err := gateway.New(cfg) // cfg is a *config.Config
if err != nil { ... }
defer gw.Close()
http.ListenAndServe(":8080", gw.Handler())A federated multi-org config — each upstream owned by a different team, in any language:
{
"upstreams": [
{
"id": "github-tools",
"url": "https://mcp.platform.acme.com/mcp",
"namespace": "gh",
"owner": { "org": "acme-platform", "team": "devex" }
},
{
"id": "ml-search",
"url": "https://mcp.ml.acquired-co.com/mcp",
"namespace": "search",
"owner": { "org": "acquired-co", "team": "ml" },
"rateLimit": { "requestsPerMinute": 600 },
"circuitBreaker": { "failureThreshold": 5, "halfOpenAfterMs": 30000 }
}
],
"server": { "rateLimit": { "requestsPerMinute": 6000 } }
}fold fans list requests out across all upstreams concurrently, merges and namespaces the results, degrades gracefully when an upstream is down (_meta["run.fold/partialFailure"] lists the failed upstream ids), and short-circuits unhealthy upstreams with a per-upstream circuit breaker. Proxied results are tagged with their origin in _meta["run.fold/upstream"]. Set REDIS_URL (or server.redisUrl) to share cache, rate-limit, and circuit-breaker state across instances — a fleet of gateways behaves as one. See fold.config.example.json for a full example.
Replicated upstreams load-balance. Give an upstream urls instead of url and the gateway balances across the replicas: MCP is sessionful, so each new session — the shared root session, and each client's bridged session — connects round-robin to the next healthy endpoint and stays pinned there. An endpoint that refuses a connection is skipped (the connect fails over to the next replica in the same attempt) and rests out of rotation for the breaker's halfOpenAfterMs before being retried. Add "healthCheck": { "intervalMs": 30000 } for active probing: the gateway connects to every endpoint on the interval, so a dead replica is ejected before the first client request and a recovered one returns to rotation without a live-request retry paying for the discovery. Per-endpoint health shows in /health and the fold_upstream_endpoint_healthy metric. Replicas are assumed identical — one namespace, one credential strategy, one policy surface.
Configuration hot-reloads. kill -HUP the process — or run with --watch to poll the config file — and fold revalidates and applies the new document without dropping the listener: the upstream set and the policy engine swap atomically, in-flight requests finish against the snapshot they started on, and connected clients receive list_changed notifications so they refetch. Upstreams whose config is unchanged keep their live sessions; removed or changed ones are drained (closed after their request timeout) and a changed upstream's resource subscriptions move to its replacement. Embedders get the same behavior via gw.Reload(cfg). The auth, server, routing, audit, tracing, and discovery sections are wired in at construction and cannot hot-swap — changing them makes the reload fail loudly, keeping the running configuration; a rejected reload never takes anything down.
Upstreams can be discovered. With the discovery section configured, fold polls a URL for {"upstreams": [...]} (same schema as the static section) and hot-swaps the discovered set into the federation alongside the statically configured upstreams — a team ships an MCP server, the registry lists it, and it appears behind the gateway without anyone touching fold's config. Discovery composes with reload: base reloads keep the discovered set, discovery syncs keep the base, and both flow through the same validated atomic swap. Fail-safe by construction: an unreachable source, a malformed document, or one that collides with a static upstream id or namespace is rejected whole and the last good set keeps serving (fold_discovery_syncs_total counts outcomes).
Request pipeline
POST /mcp
→ host validation DNS-rebinding protection (Host/Origin allowlist)
→ body cap maxBodyBytes (413 before any read), 30 s body read deadline
→ authenticate Bearer → issuer allowlist → JWKS → audience → Principal
→ rate limit global → tenant → per-principal windows → 429 + Retry-After
→ route federated fan-out (lists) or namespaced routing, tenant resolved
→ visibility tenant upstream subset (a fan-out never reaches what it excludes)
→ authorize deny-by-default policy per invocation
→ decision hook optional external allow/deny on ingress (after policy; decides, never rewrites)
→ per-upstream guards rate limit, circuit breaker, request timeout, budgets
→ proxy credentials attached, held SDK session per upstream
→ egress per-principal list filtering, namespace rewriting, optional hook on results
→ audit one event per request, including denials (single exit door)Server-initiated traffic bridges both ways. Named invocations run over a per-client upstream session whose handlers forward sampling (sampling/createMessage), elicitation, log messages, and progress notifications back to the originating client — routed over that call's own stream, so clients without a standalone SSE stream still hear them. resources/subscribe is forwarded to the owning upstream and resources/updated notifications fan back out to subscribed clients; completion/complete routes by prompt namespace or resource ownership; a client's logging/setLevel propagates to its upstream sessions. Idle per-client sessions are swept after 5 minutes, and the same sweep releases subscriptions whose downstream client disconnected without unsubscribing — the one shared upstream subscription per URI is dropped when its last live subscriber is gone. A single downstream session may hold at most 1024 subscriptions; the 1025th is refused with -32602 until one is released, which keeps a table keyed by caller-chosen URIs finite without evicting a subscription some client still depends on.
Routing headers cross the hop. A client may mirror selected tool arguments into Mcp-Param-{Name} headers so an intermediary can dispatch without reading the body, and the transport requires an intermediary that does not recognize one to forward it and otherwise ignore it. fold does both: they are relayed to the upstream and never read, so the body stays the only thing fold parses and the headers cannot become a second control surface. The relay is the narrowest thing that satisfies the rule — fold forwards no other client header, never overwrites a value the SDK generated for the call it is actually making, and attaches nothing caller-supplied to a host outside the upstream's configured endpoints.
If you route on these, validate them. The specification has a server check a param header against the request body, and that check runs only once a connection has negotiated 2026-07-28 — which fold's default upstream protocol deliberately sits below, because the sessionful handshake is what carries server-initiated traffic. So on a default deployment nobody validates them: fold forwards without reading, by design, and the upstream's own transport does not check at that protocol level. A balancer or router in front of an upstream is therefore dispatching on a caller-supplied value that fold's policy engine never saw and that need not agree with the body fold authorized. Treat them as the routing hints they are and confirm against the body before acting on one. (They also ride every request of the call that carried them, including that session's own handshake, so a value scoped to one invocation can reach a router deciding where a whole session lives.)
Multi-round-trip requests are answered, not relayed. An upstream that needs more input answers with an input-required result rather than blocking: it returns the questions it wants answered and an opaque requestState, and the exchange completes when someone retries the original request with inputResponses filled in and that state echoed back. Through fold, that someone is currently fold. Its upstream client runs the SDK's multi-round-trip middleware, so an upstream's question is fulfilled from the caller's bridged session — elicitation, sampling — and the retry is issued below the router. The caller sees one completed call and never sees the question. That is era translation rather than proxying, and for a handshake-era client talking to a modern upstream it is the only thing that can work; it is also not what a relay would do, and the difference is visible in three places. A caller whose client declares no elicitation handler gets the call failed rather than the question. resources/read cannot do it at all, because reads ride the shared root session, which has no handlers to answer with. And a caller that could have answered for itself is never asked.
Where a downstream client does supply inputResponses and requestState itself, fold forwards both verbatim: the specification requires a client to echo the exact state and forbids inspecting or modifying it, and fold is the client on the upstream leg. Nothing about this is cached, and nothing may be — a result that depended on inputs outside the cache key is not cacheable, which is why fold's caching stops at lists.
MCP Apps works through the gateway. MCP Apps (SEP-1865) is negotiated by the client: a server checks capabilities.extensions["io.modelcontextprotocol/ui"] before registering UI-enabled tools, and serves a text-only fallback to anyone who did not declare it. fold carries that declaration rather than configuring it — there is no setting for this. A named invocation rides the per-client session, which now declares what its client declared; lists ride the shared root session, which cannot hold two answers, so root sessions and their list-cache entries are keyed by a normalized capability profile. A federation whose clients are all alike keeps exactly one session per upstream, as before; one with both kinds keeps two, and neither client is ever served the other's list. The profile is computed from the extension identifiers fold implements, never from the client's raw map, so a caller inventing extension ids cannot mint sessions or cache entries.
MCP Apps interfaces stay attached to their own server. MCP Apps (SEP-1865) points a tool at an interactive interface through a ui:// resource URI in its _meta.ui — a URI the extension requires to be unique only within one server, and which the published starter templates ship with no server segment at all. Federate two upstreams built from one template and they advertise the same URI for different interfaces: fold used to serve a read of it from whichever upstream listed it last, so a host rendering one team's tool could get another team's app, and which one depended on what some other client had done first. fold now mints those URIs per namespace — ui://fold/{namespace}/{rest} — in _meta.ui (both the nested and the deprecated flat form), in resources/list, in what a read answers with, and in resources/updated notifications for a subscribed interface. Reads route straight to the owner with no probing and no dependence on request history, policy still decides on the URI the upstream published, and every other resource URI stays untouched: this is the one documented exception to fold never rewriting a URI, and it is narrow on purpose — a ui:// pointer is republished on every tools/list rather than persisted by a client. Passthrough rewrites nothing, here as everywhere.
Icons survive federation. MCP lets a server hang an icon off a tool, prompt, or resource, and it tells the client how to handle one: fetch only https or data:, reject unsafe schemes and cross-origin redirects, fetch without credentials, sniff the bytes rather than trust the declared type — and "verify that icon URIs are from the same origin as the server". That last rule is what makes an icon a federation problem rather than a field. An upstream's icon points at the upstream's origin, which behind a gateway is neither the origin the client connected to nor, for the cluster-internal upstream that is the ordinary enterprise case, an origin the client can reach at all — so a conforming client rejects every federated icon and a lenient one fails to load it, and fold looks like a server whose upstreams have no icons. fold mints its own URL for each, {server.publicUrl}/icons/{namespace}/{digest}, and serves the bytes: the same move ui:// minting makes, safe for the same reason, since an icon src is republished on every list rather than persisted by a client. Four bounds keep it from being an open proxy — only an icon on one of that upstream's own configured endpoint hosts is minted (the host bound credential attachment already uses, checked at mint and again before the fetch, so a hand-built digest cannot aim the gateway elsewhere); unsafe schemes are dropped rather than republished under fold's name; data: URIs pass through untouched; and passthrough mints nothing. The fetch carries no credentials by construction rather than by policy, refuses redirects outright, is bounded and refuses rather than truncates, and identifies the image from its magic bytes: image/png, image/jpeg, image/webp, image/gif. image/svg+xml is refused — see "Not implemented". The endpoint is unauthenticated because the specification requires clients to fetch icons without credentials and a browser <img> carries no token; what it discloses is bounded to match, and the reasoning is in docs/security-model.md. With no server.publicUrl and no auth.resource, nothing is minted and icons are served exactly as the upstream published them, which is what fold did before this existed. server.identity adds fold's own icons and websiteUrl to what it reports at initialize.
Federated tasks. Task ids are opaque and clients persist them across sessions, so — like resource URIs — fold never rewrites them; ownership is remembered instead. A tools/call that mints a task (advertised in the result _meta) pins taskId → upstream; tasks/get, tasks/cancel, tasks/result, and tasks/update route to that owner, whose errors pass through verbatim. A task fold never saw (minted out of band, or whose record has expired) is located by a read-only tasks/get probe across upstreams — the owner answers, everyone else is a healthy "no" — after which the mutating method is sent to the owner alone. tasks/list merges every upstream in deterministic id order and pages like the typed lists (routing.pageSize); ids no upstream knows answer -31045. Ownership is bound to the minting principal: another caller's task-scoped calls answer -31045 exactly like an unknown id (no existence leak, no probe), and tasks/list shows each principal only its own tasks. The ownership index is an authorization record, not a routing hint, so it lives behind state.Provider: with REDIS_URL (or server.redisUrl) set, a whole fleet reads the same ownership and the binding survives a rolling restart — a caller cannot reach another principal's task by landing on an instance that did not serve the mint. Records are keyed by a digest of the task id, hold a digest of the owning principal, and expire after 24 hours; a Redis outage falls back to that instance's locally mirrored records. Tasks fold has no ownership record for (out of band, expired, or minted on a fleet with no shared state) stay reachable by any caller via the probe fallback; anonymous callers share one owner bucket, so no-auth deployments are unaffected. The Go SDK does not yet model the task lifecycle, so these are forwarded as opaque JSON via the SDK's custom-method mechanism to fold's task contract; the wire types are gateway-local and swap for the SDK's typed task API when it ships. subscriptions/listen fan-in is not included (the SDK exposes no public API for it).
Configuration
One JSON document, validated on startup — structure and cross-field rules by
fold --validate, which needs no secrets; the things only a running process
can check (env vars named by secretRef, sink paths, Redis, the metrics
bind) at boot. Loaded from
--config <path> or FOLD_CONFIG, which accepts either a file path or the
JSON itself. A JSON Schema ships with fold
(config/fold.config.schema.json, printed
by fold --schema) for editor completion and CI linting.
Full reference: docs/configuration.md — every field, default, and the reasoning behind each one.
Section | What it configures |
Required. The federation itself: each upstream's endpoint or replica set, namespace, credential strategy, timeouts, breaker, rate limit, budget, and list-cache TTL. | |
Gateway authentication: trusted issuers, JWKS, and the token audience every caller must present — plus the embedded EMA authorization server. | |
Deny-by-default authorization: rules over server, method, name, arguments, and tool annotations, gated on principals by group, subject, issuer, claim, or OAuth scope — filtering lists and refusing calls together, in both directions. | |
The external decision endpoint: an out-of-process allow/deny on ingress, egress, and server-initiated traffic. Absent by default. | |
Where the event per terminal response goes: stdout, rotating file, webhook, or OTLP logs, with retry and dead-lettering. | |
The listener: MCP path, allowed hosts, global and per-principal rate limits, body cap, session expiry, shared state, the metrics listener, introspection, and the console. | |
The namespace separator, and the page size federated lists are served in. | |
Polling a registry for upstreams, with the allowlists that bound what a partially trusted source may ask for. | |
Groups of principals, resolved from claims the IdP already asserts, and the budget, rate limit, and upstream visibility applied to each. | |
OTLP endpoint, service name, and sample ratio for fold's own spans. Absent, the caller's trace context is still propagated. |
Error codes
Gateway-minted JSON-RPC errors (upstream errors pass through verbatim):
Code | Meaning |
| Per-upstream rate limit exceeded |
| Upstream unavailable (circuit open / unreachable / all upstreams down) |
| Policy denied the invocation. When scopes were the only thing missing, |
| Name does not resolve to a configured namespace |
| Consumption budget exhausted for the period (server or per-upstream) |
| Task id not owned by any upstream |
Observability
GET /metrics— Prometheus exposition. Gateway metrics:fold_requests_total{method,outcome},fold_request_duration_seconds{method},fold_upstream_requests_total{upstream,outcome},fold_upstream_request_duration_seconds{upstream},fold_upstream_breaker_state{upstream}(0 closed / 1 half-open / 2 open),fold_upstream_endpoint_healthy{upstream,endpoint}(multi-endpoint upstreams: 1 in rotation, 0 ejected),fold_http_rejections_total{reason},fold_jwks_fetches_total{issuer,outcome}(the inbound trust anchor:ok,error, orstalewhen a failed refresh left the last good key set serving),fold_discovery_syncs_total{outcome},fold_list_items_total{method,stage}(offered / served / capped — the context reduction per-principal filtering performs, as a ratio with a denominator),fold_build_info{version}, plus standard Go process/runtime collectors. Withtenantsconfigured, two more carry the tenant dimension:fold_tenant_requests_total{tenant,outcome}andfold_tenant_upstream_calls_total{tenant}— the second counts the unit a tenant budget is charged in, so an allowance can be watched being spent. They are separate series rather than atenantlabel on the metrics above, because label sets are frozen by the compatibility contract.Packaged interpretation, not just data — a Grafana dashboard, a
PrometheusRuleset, and documented SLOs ship in the chart (metrics.dashboard.enabled,metrics.prometheusRule.enabled). The availability SLO deliberately counts only outcomes fold failed: a denial, a rate limit, and an exhausted budget are the gateway working as configured, and an SLO that counted them would page someone for tightening a policy. A test keeps every panel and alert in lockstep with the metric names the code registers, the two rule files in lockstep with each other (expression, duration, severity, summary), and every error code the pack names in lockstep with the codes fold mints. Two alerts page —FoldDownandFoldAllRequestsFailing; the rest warn. See docs/operations.md.Distributed tracing — W3C Trace Context (
traceparent/tracestate) headers on incoming requests are always propagated to the upstream calls they cause, so the gateway hop joins the caller's trace. With thetracingsection configured, fold also emits its own OpenTelemetry spans (server span per request, client span per upstream call, pipeline outcomes as attributes) over OTLP/HTTP — the gateway hop appears in the trace instead of being invisible, and upstream calls parent under fold's client span while keeping the caller's trace id.Latency, measurably — the
benchCI job gates every merge on added p50 latency < 5 ms through the proxy path (loose for shared runners). Typical local numbers (Apple Silicon, in-process upstream): ~0.20 ms added p50, gateway p99 ≈ 0.57 ms. Reproduce:make bench.Throughput, measurably —
make loadtestsweeps one instance under 8/64/256 concurrent SDK client sessions, direct vs through-fold: ~8,500 req/stools/callat 64 connections (p99 ≤ 22 ms), 12,100 req/s at 256 (p99 ≤ 65 ms), zero errors, with the gateway process at ≈26 MiB idle and ≈0.3–0.4 MiB plus ≈7 goroutines per held session (Apple M4 Pro, loopback,main2026-09-04). Methodology, full tables, memory, and the honest caveats: docs/benchmarks.md.Structured logging — operational events (startup, upstream connect/reconnect, session drops, circuit-breaker transitions, refused cross-host redirects, SSE-hang fallbacks, shutdown) log via
log/slog.--log-format text|jsonand--log-level debug|info|warn|erroron the CLI; embedders passgateway.WithLogger(*slog.Logger). Per-request accounting stays in metrics and the audit sink, not the log stream.
Operational endpoints
GET /health— pings every upstream concurrently; reports per-upstream connectivity, latency, breaker state, owner, and — for multi-endpoint upstreams — the balancer's per-endpoint rotation state.503when no upstream is reachable — and, on a gateway whose upstreams all arrive fromdiscovery, until the first document has actually been applied: a pod whose source was down at boot has an empty federation, not a healthy one, and reports it under adiscoveryobject (lastOutcome,lastSyncAt,applied) rather than joining the Service to serve empty lists. (/healthz, the pre-v1.5 path, was removed in v1.9 — see API stability.) The fan-out is shared: concurrent callers ride one collection and the result is reused for a second, so probing this unauthenticated endpoint in a loop cannot multiply into upstream traffic. A reload or discovery sync invalidates it immediately.GET /metrics— Prometheus metrics (see Observability).GET /.well-known/oauth-protected-resource— RFC 9728 metadata (when auth is enabled).GET /.well-known/oauth-authorization-server— RFC 8414 metadata for the embedded EMA authorization server (when EMA is enabled). fold names itself in the RFC 9728 document'sauthorization_serverswhenever EMA is on, so this is where that pointer resolves: issuer, token endpoint, key set, and the one grant type it accepts.GET /api/federation— the federation snapshot (whenserver.introspection.enabled): upstream health and topology, policy shape, audit sinks, discovery status, and the viewer's tenant governance. Authenticates like/mcpand shares its rate budgets. Was/console/api/statethrough v1.8.GET /api/auth-hint— the deliberately unauthenticated sign-in hint (whenserver.introspection.enabled): issuer, client id, scopes, resource. Public SPA configuration only. Was/console/api/auththrough v1.8.GET /console/— the read-only fold console page (whenserver.console.enabled): an observability dashboard over/api/federationplus an MCP test console. The test console is a plain MCP client against the gateway's own/mcp, so policy, rate limits, and audit apply to it like any other client — there is no privileged path.
Guides
docs/configuration.md — the configuration reference: every section, field, and default, with the reasoning behind each.
docs/deploy.md — Docker, compose, Helm, systemd; TLS/SSE fronting,
allowedHostsand probes, hot reload in each shape, Redis for fleets, the production checklist.docs/operations.md — day-2 reference: every endpoint, metric, audit field, and error code, and how reloads, discovery, and probes surface in logs and metrics.
docs/security-model.md — the architecture: trust anchors, the inbound chain, the enforcement pair, credential containment, tenant isolation.
docs/discovery-controller.md —
fold-discovery, the Kubernetes producer: label a Service and it joins the federation.docs/stdio.md —
fold-stdio, the shim that puts a local stdio MCP server behind the gateway as an ordinary http upstream.docs/registry-discovery.md —
fold-registry, the MCP Registry producer: name the servers you trust and the gateway tracks their endpoints, versions, and retirements.docs/benchmarks.md — the latency gate and the throughput sweep: methodology, numbers, how to reproduce.
docs/embedding.md — the Go embedding surface, with CI-compiled examples.
docs/defaults.md — the v1.0 defaults review, every default a decision on record.
docs/roadmap.md — direction: what fold intends to build next, and what it deliberately declines.
docs/design-stdio.md — design record: why stdio upstreams arrive as a sidecar shim rather than subprocess supervision in the gateway.
docs/design-consumption.md — design record: quotas, budgets, and metering — what a gateway on the MCP path can honestly govern, and what it must refuse to guess.
docs/design-policy-depth.md — design record (shipped): deny rules, argument constraints, and destructive-operation gating — with the precedence, visibility, and trust questions each one raises settled before implementation, and the two things implementation decided that the design did not anticipate.
docs/design-decision-hook.md — design record (shipped): the external decision hook — why it decides and cannot rewrite, why
timeoutMsandonErrorhave no defaults, and why sending arguments to a second endpoint is a data-egress decision to state in those words.docs/design-mcp-apps.md — design record: MCP Apps through a federating gateway — why a client-declared extension needs capability-keyed upstream sessions, why
ui://URIs are the one kind fold rewrites, and the two gaps that belong to the extension's specification rather than to fold.docs/design-tenancy.md — design record: the tenant object — a named set of principals and the governance that applies to them, why it never becomes a trust anchor, and the two corrections implementation forced on the design.
docs/design-egress-oauth.md — design record (proposed): per-user upstream credentials — how a caller reaches a third-party SaaS MCP server as themselves without the credential ever touching their laptop, why the consent prompt is URL-mode elicitation rather than anything fold invents, and where the line sits between brokering credentials and becoming a secrets manager.
Deploying
fold is a single static binary with no local state — see docs/deploy.md for the full guide (TLS, allowedHosts, probes, Redis, secrets, audit shipping, production checklist).
Docker:
ghcr.io/fold-run/fold(multi-arch, distroless) — see Quick start.docker compose:
compose.yaml, with optional Redis, stdio-shim, and observability profiles —make compose-upruns it.Kubernetes: Helm chart in
deploy/helm/fold— probes, config-as-ConfigMap, secrets viaenvFrom, optional Ingress/HPA/PDB/ServiceMonitor.VM / bare metal: prebuilt binaries on the releases page plus a hardened systemd unit in the guide.
Repository layout
Path | Purpose |
| The |
| The Kubernetes discovery-document producer ( |
| The stdio shim: runs one local MCP server over streamable HTTP ( |
| The MCP Registry discovery-document producer ( |
| Gateway engine: pipeline, federation routing, proxying, health |
| Config schema + validation |
| OAuth resource server (JWKS verifier) + upstream credential strategies |
| Allowlist policy engine + per-principal list filtering |
| Audit events + sinks (stdout, webhook) |
| Guides and decision records — see Guides above, plus the roadmap |
| Sliding-window limiter |
| Circuit breaker |
| TTL cache with single-flight refresh |
Development
make build
make test # unit + integration (real SDK client/server fixtures)
make race # what CI runs
make cover # race tests + coverage summary (coverage.out)
make lint # golangci-lint (config in .golangci.yml)
make check # fmt-check + tidy-check + vet + build + race + lint
make bench # added-latency gate (p50 < 5 ms through the proxy path)
make fuzz # fuzz every parser an untrusted party controls: config, cursors, discovery docs, _meta, cache scope, param headers (seeds run in `make test`; drift.yml runs this weekly)
make conformance # official conformance suite through the gateway (needs node)
make compose-up # run the local stack on this host (see docs/deploy.md)CI runs these same targets (.github/workflows/ci.yml), plus govulncheck (make vuln).
The integration suite spins up real MCP servers from the official Go SDK behind the gateway and exercises federation, namespacing, policy filtering and denial, partial failure, credential injection (static and passthrough), rate limits, the breaker, JWT auth against a fixture JWKS issuer, RFC 9728 metadata, and the full server-initiated bridging loop (sampling, elicitation, logging, progress).
API stability
This is the v1 compatibility contract, in force as of v1.0.0.
Frozen at v1.0 (breaking changes only with a new major version):
The config document — field names, meanings, defaults, and validation semantics. The machine-readable contract is
config/fold.config.schema.json(fold --schema), kept in lockstep with the code by test. Defaults are part of the freeze — every one was reviewed as a deliberate decision before v1.0 (docs/defaults.md).The
foldCLI — flags, exit codes, andFOLD_CONFIGsemantics.The wire surface — gateway-minted JSON-RPC error codes, HTTP endpoints (
/mcp,/health,/metrics,/api/federation,/api/auth-hint,/icons/*,/.well-known/*,/oauth/token), metric names and label sets, and the audit event JSON shape. The/api/federationresponse shape is frozen with the rest: fields may be added within a major, none removed or renamed — it has an out-of-tree consumer that can skew against the gateway, which is what makes it a contract rather than an internal detail./healthzwas the health path through v1.4, a deprecated alias from v1.5.0, and removed in v1.9.0; point probes at/health.Go, for embedders — the
gatewaypackage (New,Option,WithLogger,Gateway.Handler/Reload/Close,Version), theconfigpackage's document structs andLoad/Parse/Validate/Schema, plus the contract types the gateway hands outward:auth.PrincipalwithWithPrincipal/PrincipalFromContext, andaudit.Event/Outcome. See the package example.
Wiring, not API (may change in any release): the constructors the gateway threads through its packages — auth.Verifier/EMA/UpstreamCredentials, policy.Engine, audit.Logger/Sink. They are exported so the gateway can reach them across package boundaries, not as an extension surface. internal/ packages are never API.
Upgrades and deprecation. fold follows semver: within a major version, upgrades are drop-in — new config fields and capabilities arrive in minors, nothing frozen changes. Anything slated for removal is deprecated in a minor release (documented here and in the changelog, with the replacement) and removed no sooner than the next major. Security fixes land on the latest minor as patch releases (see SECURITY.md).
One break after v1.9.0, and why. The gateway-minted error codes changed once more after this contract took effect: the 2026-07-28 protocol revision reserved the range they occupied, and one of them — -32042 — had already been redefined as "URL elicitation required", a meaning shipped SDKs act on by prompting the user and retrying. A policy denial that a conforming client turns into a prompt and a retry is not something a gateway can wait a major version to fix. The codes moved outside the JSON-RPC reserved range (only the thousands digit changed); the reasoning and the full mapping are in the changelog. The same justification as below applies: no users yet.
This contract applies from v1.9.0 forward. v1.9.0 itself makes breaking changes to config field names and endpoint paths without a deprecation window — the one release that does. fold had no users to protect at that point, and taking the break there rather than after launch was the honest trade; the alternative was a major-version bump whose module-path change would have silently pinned every published go run …@latest to v1.8.0 forever. See the v1.9.0 changelog entry for the migration.
Not implemented
Known gaps, documented deliberately:
SEP-2575
subscriptions/listenstreams — the Go SDK supports the 2026-07-28 protocol on its streamable HTTP server only in stateless mode, which fold cannot use: session-keyed bridging (sampling, elicitation, per-client streams) requires stateful sessions. Clients on the legacy handshake — which is what the SDK negotiates against stateful servers today — get full notification fan-in (list-changed and resource-updated, tested ingateway/listen_test.go). fold's fan-in already sits on the surfaces the SDK reuses for listen streams, and a drift canary in that test fails when the SDK lifts the restriction. Federated tasks (get/list/cancel/result/update with mint-affinity and probe fallback) are implemented — see above.Multi-round-trip requests are terminated at the gateway, not proxied — described under Request pipeline. fold's upstream client runs the SDK's multi-round-trip middleware, so an upstream's input request is answered from the caller's bridged session and the retry is issued below the router; the caller never sees the question. That is the right behavior for a handshake-era caller talking to a modern upstream, and the wrong one for a caller that speaks the pattern itself. Three consequences follow and none is yet addressed: a client declaring no elicitation handler has the call failed rather than being asked;
resources/readcannot complete an input-required exchange at all, because reads ride the shared root session and it carries no handlers; and a modern-era caller is never given the chance to answer. Turning fold into a relay is a one-line change to the client options and a large change to what the bridged sessions are for, so it waits on the open half of roadmap item 13: the handshake-era check shipped aspolicy.serverInitiatedDecision, and governing theinputRequestsa relayed result carries — the same decision at the call site the next protocol era moves it to — is the half nobody has designed yet.What the
2026-07-28era changes, and fold has not — none of it reachable today, because the SDK serves that revision only on stateless HTTP servers and fold runs stateful;gateway/era_test.goandgateway/listen_test.gopin the refusal from both sides, and each becomes the notice to do this work. A client's session-lessserver/discoverprobe is answered405before the SDK handler sees it — the refusal the SDK client reads as "fall back toinitialize", and the same one fold's upstream leg gives upstreams — because the handler would otherwise mint a session for the probe that nothing ever closes. fold advertises noextensionsin its server capabilities, so a client following the tasks extension's negotiation rules would not declare the extension and would never reach fold's federated tasks. It federatestasks/listandtasks/result, which the redesigned extension removed in favour of pollingtasks/get. It still advertisesresources/subscribeand still handleslogging/setLevel, both of which that revision retired — the level moved to a per-request_metakey fold has no forwarding path for. And a result fold relays from an upstream carries noresultTypeof its own, so the SDK stamps"complete"on it, which would be wrong for a relayed input-required result. All of it is correct for the era fold actually serves and wrong for the next one; the ordering is deliberate rather than neglectful.An upstream's own caching hints are not honoured — MCP lets a server declare how long a list may be considered fresh (
ttlMs) and who may share it (cacheScope). fold reads neither: its list cache is bounded by the operator'scacheTtlMsalone, so an upstream declaring one second of freshness is served from fold's cache for the default thirty. The cause is structural rather than an oversight — the SDK's paginating iterators yield items and consume the result envelope the hints ride on, so fold never sees them, andstate.ListCache.GetOrFilltakes its TTL before running the fill that would produce one. Honouring them means changing astate.Providerinterface with two implementations. The mirror-image decision on the outbound side is made and documented ingateway/cachehint.go; this is the same argument owed on the inbound side. ThecacheScopehalf is a narrower gap than it looks: with a gateway-configured credential every caller is one authorization context from the upstream's point of view, and fold already disables list caching entirely where that is not true.Icons: three narrower gaps, all deliberate. SVG icons are refused rather than proxied. Two arguments, either sufficient. fold would be serving the SVG from fold's own origin, where an SVG's embedded script runs same-origin with
/console/,/api/federation, and/oauth/token— strictly worse than the upstream serving it, since the upstream's origin holds nothing of fold's. And structurally, SVG is XML and has no magic bytes, so the strict magic-byte allowlist the specification asks a consumer to maintain cannot admit it without either trusting the declared content type or parsing the XML, and the second is the content inspection below. An upstream whose only icon is an SVG renders as no icon. Resource-template icons are not minted — templates are the one list fold passes through with no copy at all, so minting them means a new memo for the rarest case; they are served as the upstream published them, which is the pre-icons behaviour. And passthrough rewrites nothing, so a single un-namespaced upstream's icons keep pointing at that upstream — which is what a client talking to it directly would see, and therefore what passthrough means.An upstream's
instructionsare discarded. MCP lets a server send guidance for the model alongside its capabilities, and fold reads no upstreamInitializeResultat all:Gateway.instructionssynthesizes fold's own text from the namespace list, so a client is told how names are namespaced and nothing whatsoever about what the upstreams behind them are for. That is real content loss for a federating gateway and it is not a hard problem — the open question is what merging N instruction blocks into one honestly looks like, since concatenation grows without bound and picking one upstream's is arbitrary. No design has been argued yet, which is why this is a gap rather than a plan.Progress is bridged only on
tools/callandprompts/get.notifications/progressreaches a caller over its bridged session, andbridgeForhas exactly three call sites. Everything else —resources/read, every list,completion/complete, and the federatedtasks/*— runs on the shared root session, whose client options carry no progress handler, so an upstream reporting progress on a long read reports it into nothing. This is the same root cause README already records for multi-round-trip requests ("reads ride the shared root session and it carries no handlers"); the progress consequence was not stated. It also requiresprotocol: "session"and a stateful downstream session on both legs.A stdio front door — fold's client-facing transport is streamable HTTP only.
fold-stdioputs a stdio server behind the gateway; nothing puts the gateway in front of a stdio-only client, which needs a bridge such asmcp-remote. This is a decision rather than a gap awaiting work — a local process per user sits awkwardly with "one governed endpoint" and complicates principal attribution and audit — and it is recorded as a non-goal in docs/roadmap.md and in docs/design-stdio.md.Content inspection (DLP / PII filtering / prompt-injection detection) — deliberately out of scope. Inspecting request and response bodies means buffering and rewriting traffic, which conflicts with fold's invisibility rule (behavior through the gateway matches hitting the upstream directly) and its latency gate — and inline detection is a product of its own, not a gateway feature. fold's security model is structural instead: deny-by-default tool allowlists, per-principal invisibility, claim-gated (ABAC) rules, credential brokering so agents never hold upstream keys, and a full audit trail to feed the SIEM that does the detecting. When inline inspection is genuinely table stakes, the fold-shaped answer is the opt-in decision hook — an external policy endpoint on the ingress, egress, and server-initiated paths — not built-in scanning. It decides and cannot rewrite, which is what keeps the invisibility rule intact while still giving an organization somewhere to put its own judgement.
MCP Apps: two protocol-level gaps — the extension gives an app no way to learn the name its host knows a tool by, so an app that hardcodes one cannot call it once federation has namespaced it (verified: the reference host forwards the bare name and fold answers
-31043). And the host's rule that an app may not call tools across servers has no meaning when every upstream arrives on one connection, because the specification defines no way for a gateway to tell an app-initiatedtools/callfrom a model-initiated one. Both need answers in the extension itself — every aggregator has the second hole and none can close it alone — so they are reports to file rather than features to build. Everything on fold's side of the line ships: capability negotiation andui://routing, above. Design record: docs/design-mcp-apps.md.
The gaps that carry a plan or a dependency appear in docs/roadmap.md — subscriptions/listen with the SDK dependency it waits on, content inspection as a standing non-goal, and the MCP Apps pair in the gated horizon, waiting on the extension's own specification — alongside the rest of what fold does and does not intend to build. The three added with icons carry neither: the SVG refusal is a decision, and the other two are honest reports of things nobody has argued a design for yet.
Changelog
Release history, newest first: CHANGELOG.md.
License
Apache-2.0
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.
MCP Gateway: wrap any MCP server with cold-start retries, uptime SLA, and per-execution MPP billing.
- gatewayOAuthai.sealgate
MCP gateway with runtime security policy, tool-call-level control, and audit of agent actions.
Unified gateway exposing 150+ tools across all NexGenData MCP servers via one endpoint.
Related MCP Servers
- -licenseNot gradedqualityNot gradedmaintenanceA feature-rich Model Context Protocol gateway that federates MCP and REST services, unifying discovery, authentication, and transport protocols while providing virtualization of legacy APIs as MCP-compliant tools.-
- FlicenseNot gradedqualityDmaintenanceA centralized gateway and router that integrates multiple MCP servers into a single endpoint with built-in policy enforcement and secret management. It features a Web GUI for managing tool access, audit logs, and multi-environment configurations across various sub-servers.-
- AlicenseNot gradedqualityDmaintenanceA unified MCP gateway that aggregates multiple MCP servers and API plugins behind a single endpoint with authentication, rate limiting, audit logging, REST API bridge, and web dashboard.8MIT
- FlicenseNot gradedqualityCmaintenanceA unified enterprise-grade gateway that lets LLM agents call multiple MCP servers through a single REST API, with built-in authentication, rate limiting, logging, and metrics.-
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/fold-run/fold'
If you have feedback or need assistance with the MCP directory API, please join our Discord server