Skip to main content
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 your config.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

Use mcp.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:
  • false for a boolean
  • An empty string
  • 0 for a number
  • 0s for 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.stateless defaults to true inside mcp.servers, the same as the deprecated top-level mcp.session.stateless.
  • oauth.max_scope_combinations defaults to 2048 inside mcp.servers, the same as the top-level default.

Path Rules

Each server’s path must follow these rules:
  • It must start with /.
  • It must not start with //.
  • It must not be / alone. Every mcp.servers entry 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 unknown storage.provider_id, a duplicate path, or any other config error stops the router from starting. Fix the reported server before you restart.
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 in mcp.servers keeps running.
A per-server reload failure never fails the router or the config reload. This applies to every reload error, including an unreadable operations directory, a scope computation failure, and a tool registration failure. Alert on the error log for the affected server. Do not rely on router or config reload failure to detect an MCP problem.
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

Without a matching audience, the router accepts a token minted for one server on every other server behind the same load balancer. Set oauth.jwks[].audiences on every server that enables OAuth to prevent this.
Each server’s base_url and path together determine the resource identifier it publishes and the URL where its RFC 9728 metadata lives. Take this server:
This server publishes:
  • Resource identifier: https://billing.example.com/billing/mcp
  • RFC 9728 metadata: https://billing.example.com/.well-known/oauth-protected-resource/billing/mcp
Set 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-level mcp 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:
When 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_name
  • mcp.storage
  • mcp.exclude_mutations
  • mcp.enable_arbitrary_operations
  • mcp.expose_schema
  • mcp.omit_tool_name_prefix
  • mcp.oauth
  • mcp.router_url
  • mcp.resource_documentation
  • mcp.server.title
  • mcp.server.description
  • mcp.server.version
  • mcp.server.discover.instructions
  • mcp.session
The router’s warning does not name 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.
When you do not set graph_name explicitly, moving to mcp.servers changes the identity your MCP server advertises. graph_name feeds the Name field of MCP serverInfo as wundergraph-cosmo-<kebab-case graph_name>. In the map form, graph_name defaults to the map key, not to the old top-level graph_name value. Some MCP clients store trust or configuration against this name. Set graph_name explicitly on the entry to keep the name your clients already trust.
Before, using the deprecated top-level options:
After, using 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 the file_system provider is supported:
Then reference this storage provider in your MCP configuration:
A storage provider must be specified to load GraphQL operations. See Storage Providers for more details on configuring storage providers.

Server Discovery

The MCP server implements the server/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:
In 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 the Mcp-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:
  1. The Router returns a unique Mcp-Session-Id response header when a session is established
  2. Clients must include that value in subsequent requests as the Mcp-Session-Id request header
  3. Your load balancer or reverse proxy must route requests with the same Mcp-Session-Id to the same instance
For details, see your load balancer or reverse proxy documentation (e.g., F5 NGINX Plus - MCP Session Affinity). In 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 sets Access-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 with mcp.servers, see Running Multiple MCP Servers.