Route multiple upstreams
Problem: you have more than one MCP server (filesystem, git, databases) and you want one mcpgw endpoint that routes calls to the right backend based on the tool name.
Solution: declare each backend in upstreams[], then add routes[] entries that match by tool prefix, exact name, glob, regex, or explicit name list.
Recipe
default_upstream: filesystem # catches initialize, tools/list, ping, etc.
upstreams:
- name: filesystem
url: http://mcp-fs:9000
- name: git
url: http://mcp-git:9001
- name: db
url: http://mcp-pg:9002
routes:
- match: { tool_prefix: "fs_" }
upstream: filesystem
- match: { tool_prefix: "git_" }
upstream: git
- match: { tool_glob: "db_*" }
upstream: db
A tools/call for fs_read goes to filesystem. A call for git_log goes to git. A call for an unknown prefix returns HTTP 404 with JSON-RPC error -32601 no_route.
Why default_upstream matters
Spec-compliant MCP clients send several methods at startup that have no tool name:
initialize— handshaketools/list— enumerationresources/list,prompts/list— capability discoveryping— keep-alive
These cannot be matched by tool_prefix. Without default_upstream, the client fails at the initialize handshake with 404 no_route and you spend an afternoon debugging “why does Claude Desktop refuse to connect.”
Set default_upstream to whichever backend is canonical for your environment — usually the one whose tools/list you most want clients to see. Multi-upstream tools/list aggregation is on the roadmap; today the gateway routes the entire response from a single upstream.
Pitfalls
- First match wins. Put more specific matchers before less specific ones (
fs_admin_beforefs_). - Matchers are case-sensitive.
Fs_does not matchfs_. - Use one matcher per route. Supported route matchers are
tool_prefix,tool_name,tool_glob,tool_regex, andtool_name_in. - Upstream URLs must be reachable from inside the mcpgw container. Use the orchestrator’s service DNS (Docker Compose service name, Kubernetes Service, etc.), not
localhost.
Verifying
# Watch a tools/list — should hit the default_upstream
curl -s -X POST http://localhost:7332/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
| jq '.result.tools[].name'
# A tools/call should go to the prefix-matched upstream
curl -s -X POST http://localhost:7332/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"git_log","arguments":{}}}'
Span mcp.upstream confirms which backend handled each call.
Related
- Reference: configuration schema
- Explanation: architecture — request routing in detail