This page covers two configuration forms. Multiple MCP Servers documents
mcp.servers, the current way to configure MCP. The rest of this section documents the single-server, top-level
options. These options are deprecated. See Migrating from the Deprecated Top-Level Options.Basic Configuration
To enable MCP in your Cosmo Router, add the following to yourconfig.yaml:
Configuration Options
For OAuth-specific configuration, see OAuth 2.1 Authorization.
Environment Variables
All MCP options can also be set via environment variables:
For OAuth-related environment variables, see OAuth Configuration Reference.
These environment variables set the deprecated top-level options only. The
mcp.servers map is YAML-only.
Environment variables cannot address a map entry. Set every field of a server entry directly in config.yaml.Running Multiple MCP Servers
Usemcp.servers to run one or more MCP servers from a single router instance. Each server gets its own:
- Path
- Operations directory
- OAuth configuration (optional)
- Base URL (optional)
mcp.servers is a map. Each key names one server. Each value configures that server.
mcp.enabled must be true for any server in mcp.servers to start. Set it once, at the top level. When
mcp.enabled is false, no server starts, whatever the entries say.
Every server in mcp.servers shares:
- One listener:
mcp.server.listen_addr. - The router’s global CORS configuration. See CORS.
Server Entry Fields
A field inside an
mcp.servers entry takes only the value you give it in YAML. It does not read the MCP_*
environment variables documented for the matching top-level option elsewhere on this page. An unset field
normally takes its Go zero value:falsefor a boolean- An empty string
0for a number0sfor a duration
session.stateless and oauth.max_scope_combinations are exceptions. The router applies their documented
default when you leave them unset in an mcp.servers entry, so they match the deprecated top-level default:session.statelessdefaults totrueinsidemcp.servers, the same as the deprecated top-levelmcp.session.stateless.oauth.max_scope_combinationsdefaults to2048insidemcp.servers, the same as the top-level default.
Path Rules
Each server’spath must follow these rules:
- It must start with
/. - It must not start with
//. - It must not be
/alone. Everymcp.serversentry shares one listener. Go’s router treats/as a catch-all that matches every request, including requests meant for another server. A future release that gives each server its own listener can lift this restriction. - It must not end with
/. - It must not contain a wildcard character (
{,}, or*). - It must not start with
/.well-known/oauth-protected-resource. The router reserves this prefix for OAuth metadata. - It must be unique among enabled servers. Two servers can use the same path when at least one of them is disabled.
Failure Isolation
An operations directory that the router cannot read affects only its own server. That server starts, serves the built-in tools, and serves no operation tools. The router logs an error naming the server. Every other server inmcp.servers keeps running.
When the router reloads its configuration, it also reloads every server in mcp.servers. A server whose scope
computation fails during reload keeps its previous tools. The router removes a server’s old tools before it
registers the new ones. A tool registration failure during reload can therefore leave that server with fewer tools
than before. Both failures log an error naming the server. The router reload always succeeds, even when one MCP
server fails to reload.
Multiple Servers with OAuth Behind a Load Balancer
Each server’sbase_url and path together determine the resource identifier it publishes and the URL where its
RFC 9728 metadata lives. Take this server:
- Resource identifier:
https://billing.example.com/billing/mcp - RFC 9728 metadata:
https://billing.example.com/.well-known/oauth-protected-resource/billing/mcp
oauth.jwks[].audiences to the resource identifier, https://billing.example.com/billing/mcp in this example.
The router rejects a token whose aud claim does not match, even when the token is valid for another server on
the same router.
A load balancer or reverse proxy in front of the router can change the host in these URLs. It must not change the
path. The router derives both URLs from base_url and path. It does not know the load balancer’s own hostname.
Migrating from the Deprecated Top-Level Options
The top-levelmcp options documented in Basic Configuration and
Configuration Options are deprecated. mcp.server.listen_addr and mcp.server.base_url
are not deprecated. They configure the shared listener and its default base URL for every server.
When mcp.servers has one or more entries, the router ignores every deprecated top-level option and logs a warning
naming each one you set:
mcp.servers has no entries, the deprecated options build one server on /mcp, so an existing config keeps
working unchanged.
The deprecated options are:
mcp.graph_namemcp.storagemcp.exclude_mutationsmcp.enable_arbitrary_operationsmcp.expose_schemamcp.omit_tool_name_prefixmcp.oauthmcp.router_urlmcp.resource_documentationmcp.server.titlemcp.server.descriptionmcp.server.versionmcp.server.discover.instructionsmcp.session
mcp.session. mcp.session.stateless carries a default of true, so the
router cannot tell a value you set from the default. The router still ignores it. mcp.session.stateless: false
at the top level has no effect once mcp.servers has entries, and the router does not warn you. Set
session.stateless on each entry that needs it.
mcp.router_url only applies to the deprecated single-server form. It has no effect once mcp.servers has
entries. The router uses its own GraphQL endpoint for every server in the map.
Before, using the deprecated top-level options:
mcp.servers. The entry sets graph_name: 'mygraph' explicitly, so serverInfo still reports
wundergraph-cosmo-mygraph:
Storage Providers
MCP loads operations from a configured storage provider. Currently, only thefile_system provider is supported:
Server Discovery
The MCP server implements theserver/discover method. This method arrived in MCP protocol version 2026-07-28 as the successor to the initialize handshake. Clients call it to read the supported protocol versions, capabilities, and identity in one stateless request.
The discover.instructions option sends natural-language guidance to MCP clients. Use it to tell AI agents how to work with your graph:
mcp.servers, set discover.instructions on each entry. Each server sends its own instructions and does not
inherit them from another server or from the deprecated mcp.server.discover.instructions.
The server sends the instructions to every MCP client. This covers clients that connect with server/discover and clients that still use the legacy initialize handshake.
The router advertises protocol version 2026-07-28 by default. In session-based mode (session.stateless: false), clients negotiate 2025-11-25 or older.
Session Handling
The MCP server uses the Streamable HTTP transport and maintains per-session state via theMcp-Session-Id header. When deploying multiple Router instances, you need sticky sessions to ensure all requests for a session reach the same instance.
To configure sticky sessions:
- The Router returns a unique
Mcp-Session-Idresponse header when a session is established - Clients must include that value in subsequent requests as the
Mcp-Session-Idrequest header - Your load balancer or reverse proxy must route requests with the same
Mcp-Session-Idto the same instance
mcp.servers, sticky sessions apply per server. Each server keeps its own Mcp-Session-Id values. Route on
the Mcp-Session-Id header together with the request path. This keeps a session for /mcp/support from ever
reaching the /billing/mcp server.
CORS
The MCP server automatically configures CORS to allow cross-origin requests from MCP clients. It setsAccess-Control-Allow-Origin: * and allows the required MCP headers (Mcp-Protocol-Version, Mcp-Session-Id, Authorization, Last-Event-ID). The Mcp-Session-Id and WWW-Authenticate headers are exposed in responses. If you have additional CORS headers configured on the router, they are merged with the MCP-specific headers.
CORS is a property of the shared listener,
mcp.server.listen_addr. Every server in mcp.servers uses the same
CORS configuration. There is no per-server CORS setting.Full Configuration Example
This example uses the deprecated top-level options, in the single-server form. For an example withmcp.servers, see Running Multiple MCP Servers.