Skip to content

Try the live demo

https://demo.fold.run/mcp is a real fold gateway — the unmodified release binary — federating three public MCP servers. No signup, no key: point any MCP client (or curl) at it. Rate-limited, unauthenticated, no warranty.

NamespaceUpstreamNotes
cfdocs__*Cloudflare’s MCP docs serverA real third-party upstream
git__*GitMCPA public upstream two revisions behind — behind the gateway, just another namespace
jobs__*fold-demo-tasksA task-minting server (Go, on the official SDK); where the federated-tasks story runs live

Every example below is plain curl. Responses arrive as a one-event SSE body — read the data: line.

fold’s client side is the official Go SDK’s streamable HTTP server, so requests ride a session: initialize once, capture the Mcp-Session-Id response header, send it on everything after.

Terminal window
DEMO=https://demo.fold.run/mcp
SID=$(curl -s -D - -o /dev/null -X POST $DEMO \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"me","version":"0"}}}' \
| grep -i mcp-session-id | tr -d '\r' | cut -d' ' -f2)
curl -s -o /dev/null -X POST $DEMO -H "mcp-session-id: $SID" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

The initialize result already shows the gateway working: serverInfo.name is fold, and the instructions line names the three namespaces.

Terminal window
curl -s -X POST $DEMO -H "mcp-session-id: $SID" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

One merged tool list — cfdocs__*, git__*, and jobs__* side by side. fold fanned the request out to all three upstreams, namespaced the names, and merged the results in one page (deterministic order, cursor-paginated past routing.pageSize). If an upstream were down you’d still get the other two, with the failure named in _meta["run.fold/partialFailure"] instead of a dead endpoint. Repeat calls inside the TTL are served from the list cache.

Terminal window
curl -s -X POST $DEMO -H "mcp-session-id: $SID" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"jobs__start_job","arguments":{"label":"my job","seconds":15}}}'

The minted task rides the result’s _meta, origin-tagged by the gateway:

{ "result": {
"_meta": {
"task": { "taskId": "demo-job-2", "status": "working", "label": "my job", "remainingMs": 14999, "createdAt": "…" },
"pollIntervalMs": 1000,
"run.fold/upstream": "demo-tasks"
},
"content": [{ "type": "text", "text": "started demo-job-2 (\"my job\", 15s) — poll it with tasks/get" }] } }

Because the mint is visible in _meta, fold pinned taskId → upstream affinity as the response passed through — and a tool call is a tool call, so deny-by-default policy would have applied at mint and the call is in the audit trail.

Terminal window
curl -s -X POST $DEMO -H "mcp-session-id: $SID" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":4,"method":"tasks/get","params":{"taskId":"demo-job-2"}}'

tasks/list shows the merged view across the whole federation — your job in task-id order, with _meta["run.fold/partialFailure"] naming the two upstreams that don’t speak tasks at all:

Terminal window
curl -s -X POST $DEMO -H "mcp-session-id: $SID" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":5,"method":"tasks/list"}'

No tool name, no routing hint — fold resolves the owner from its affinity index, or by a read-only probe across upstreams for a task it never saw minted (try it: task ids survive sessions, so tasks/get from a brand-new session still finds the owner). Mutating methods are never fanned out; tasks/cancel and tasks/result locate first, then act on the owner alone. The mechanism is the subject of the launch post.

4. The older upstream you address the same way

Section titled “4. The older upstream you address the same way”
Terminal window
curl -s -X POST $DEMO -H "mcp-session-id: $SID" \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"git__fetch_generic_documentation","arguments":{"owner":"modelcontextprotocol","repo":"go-sdk"}}}'

GitMCP still speaks MCP 2025-03-26 — two revisions behind the 2025-11-25 this gateway negotiates, whatever you ask for at initialize. fold — built on the official Go SDK on both sides of the proxy — holds its own client session to it at the version it speaks, so from where you’re standing it’s just another namespace in the same tool list, behind the same governance.

Open demo.fold.run/console — the read-only fold console, enabled in the demo’s config. The upstreams table shows a row per upstream — namespace, source, auth, connected, breaker, latency — and the map view draws the federation itself: one gateway node fanning out to cfdocs, git, and jobs, each route labelled with the latency that upstream is answering in, over a footer reading 3 of 3 connected · every route crosses auth, policy and audit. It’s generated from /api/federation, so it’s this demo’s real topology, not a picture of one. The test console is a plain MCP client against the same /mcp endpoint you’ve been curling, governed and audited like any other caller.

Everything above is unauthenticated on purpose. https://enterprise.fold.run is the same binary over the same three upstreams with auth.mode: required — a separate deployment because auth.mode is gateway-wide, so making one governed would otherwise take the copy-pasteable one away.

An anonymous initialize there answers 401 with an RFC 9728 challenge, which is the correct answer rather than a failure:

Terminal window
curl -si -X POST https://enterprise.fold.run/mcp \
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"me","version":"0"}}}' \
| grep -i www-authenticate
# www-authenticate: Bearer resource_metadata="https://enterprise.fold.run/.well-known/oauth-protected-resource"

Follow that metadata and you get the issuer a client should sign in to — the discovery path an MCP client walks on its own:

Terminal window
curl -s https://enterprise.fold.run/.well-known/oauth-protected-resource
# {"resource":"https://enterprise.fold.run","authorization_servers":["https://oauth.work"],...}

Two tenants sit on it, differing on every axis a tenant governs:

TenantBudgetRate limitUpstreams
acme5,000 upstream calls/day120 req/minall three
globex1,000 upstream calls/day60 req/mincf-docs, demo-tasks — never reaches GitMCP

globex’s subset filters the fan-out before policy runs, so GitMCP is not asked, not billed, and not a partial failure when it is down. Policy is deny-by-default and splits each tenant by actor_type, so a signed-in person and that person’s agent get different surfaces through the same gateway. The console reads it all back live, behind sign-in.

The demo is assembled from one config — the same shape as any fold deployment (configuration reference):

{
"upstreams": [
{ "id": "cf-docs", "url": "https://docs.mcp.cloudflare.com/mcp", "namespace": "cfdocs" },
{ "id": "gitmcp", "url": "https://gitmcp.io/docs", "namespace": "git" },
{ "id": "demo-tasks", "url": "https://tasks.fold.run/mcp", "namespace": "jobs" }
],
"server": {
"rateLimit": { "requestsPerMinute": 300 },
"introspection": { "enabled": true },
"console": { "enabled": true }
}
}

go run github.com/fold-run/fold/cmd/fold@latest --config fold.config.json gives you the same thing in front of your own servers — see Getting started.