A large enterprise typically ends up with MCP servers owned by different teams: a CRM team’s server, a data platform team’s server, a support team’s server. Two problems emerge at this scale that a single server never has to think about: tool name collisions, and, once a federation gateway is introduced to solve that, correctly handling authentication pass-through and partial backend failure at the gateway itself.
Namespacing to Prevent Collisions
The simplest convention is a domain or team prefix baked into every tool name at registration time.
# CRM team's server
@mcp.tool(name="crm.search_accounts")
async def search_accounts(query: str) -> list[dict]: ...
# Support team's server, independently owned, no coordination needed
@mcp.tool(name="support.search_tickets")
async def search_tickets(query: str) -> list[dict]: ...
flowchart LR
subgraph collision["Without namespacing"]
S1["CRM server: search"]
S2["Support server: search"]
Agent1["Agent"] -.->|"ambiguous"| S1
Agent1 -.->|"ambiguous"| S2
end
subgraph namespaced["With namespacing"]
S3["CRM server: crm.search_accounts"]
S4["Support server: support.search_tickets"]
Agent2["Agent"] -->|clear| S3
Agent2 -->|clear| S4
end
style S1 fill:#fef2f2,stroke:#dc2626,color:#0F172A
style S2 fill:#fef2f2,stroke:#dc2626,color:#0F172A
style S3 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
style S4 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
If teams cannot coordinate on registering pre-namespaced tool names (common when adopting third-party or open-source MCP servers you do not control), the federation layer itself can rewrite names on aggregation instead.
Building a Federation/Aggregation Layer
A federation layer is itself an MCP server: it connects to N backend MCP servers as a client, aggregates their tools/list responses (rewriting names to enforce namespacing if needed), and routes incoming tools/call requests to the correct backend based on the namespaced name.
from mcp.client.session import ClientSession
class MCPFederationGateway:
"""Presents N backend MCP servers as one aggregated catalog."""
def __init__(self, backends: dict[str, str]):
self.backends = backends # {"crm": "https://mcp.internal/crm", "support": "https://mcp.internal/support"}
self._sessions: dict[str, ClientSession] = {}
self._unavailable: set[str] = set()
self._last_good_catalog: dict[str, list[dict]] = {} # namespace -> cached tools
async def connect_all(self):
for namespace, url in self.backends.items():
try:
self._sessions[namespace] = await ClientSession.connect(url)
except Exception:
self._unavailable.add(namespace)
async def list_tools(self) -> list[dict]:
"""Aggregate and namespace tool names from every healthy backend;
fall back to a cached catalog for a backend that is down."""
all_tools = []
for namespace, session in self._sessions.items():
if namespace in self._unavailable:
# Isolate the failure: serve a stale-but-labeled catalog for this
# one namespace rather than failing the whole aggregated response.
all_tools.extend(self._last_good_catalog.get(namespace, []))
continue
try:
backend_tools = await session.list_tools()
for tool in backend_tools:
if not tool.name.startswith(f"{namespace}."):
tool.name = f"{namespace}.{tool.name}"
self._last_good_catalog[namespace] = backend_tools
all_tools.extend(backend_tools)
except Exception:
self._unavailable.add(namespace)
all_tools.extend(self._last_good_catalog.get(namespace, []))
return all_tools
async def call_tool(self, name: str, arguments: dict, auth_header: str) -> dict:
"""Route a namespaced call to its owning backend server, passing
the caller's own auth through rather than using a gateway-wide credential."""
namespace, _, local_name = name.partition(".")
if namespace in self._unavailable:
return {"isError": True, "retryable": True, "message": f"Backend '{namespace}' is currently unavailable"}
session = self._sessions.get(namespace)
if session is None:
return {"isError": True, "retryable": False, "message": f"Unknown namespace '{namespace}'"}
return await session.call_tool(local_name, arguments, headers={"Authorization": auth_header})
flowchart TB
Agent["Agent"] -->|"single connection,\nown bearer token"| Gateway["MCP Federation Gateway"]
Gateway -->|"passes caller's token through,\nnever substitutes its own"| CRM["crm MCP server"]
Gateway -->|"passes caller's token through"| Support["support MCP server"]
Gateway --> Data["data-platform MCP server\n(currently down,\nserving cached catalog)"]
Gateway --> KB["knowledge-base MCP server"]
style Agent fill:#EEF0F7,stroke:#6366F1,color:#0F172A
style Gateway fill:#f0fdf9,stroke:#0D9488,color:#0F172A
style CRM fill:#fff7ed,stroke:#f59e0b,color:#0F172A
style Support fill:#fff7ed,stroke:#f59e0b,color:#0F172A
style Data fill:#fef2f2,stroke:#dc2626,color:#0F172A
style KB fill:#fff7ed,stroke:#f59e0b,color:#0F172A
Authentication Pass-Through: A Gateway-Specific Pitfall
A federation gateway introduces a subtle authorization risk that a single MCP server never has: it is tempting to have the gateway authenticate once (with its own service credential) and then call every backend on the gateway’s authority, rather than passing the original caller’s identity and scopes through to each backend. This defeats Module 4’s per-caller authorization entirely, from each backend’s point of view, every request appears to come from “the gateway,” with the gateway’s own presumably broad access, rather than from the specific end caller whose actual scopes should be enforced. The call_tool method above avoids this by explicitly forwarding the caller’s own auth_header to the backend on every routed call, so each backend continues to enforce Module 4’s authorization exactly as if the agent had connected to it directly, the gateway adds routing and aggregation, not a privilege escalation shortcut.
Partial Failure Isolation, and When Direct Connections Are Still Fine
The list_tools and call_tool methods above both isolate a single backend’s failure rather than letting it cascade into a total gateway failure, an unavailable data-platform server results only in stale (clearly cached) tools for that one namespace and a clear retryable error if actually called, while the CRM, support, and knowledge-base namespaces continue serving normally. This mirrors the same graceful-degradation principle Lesson 24 applies at the agent’s own multi-session layer, applied here at the gateway instead.
Federation adds an operational component (the gateway itself needs deployment, monitoring, and its own auth pass-through logic, as shown above). For a small number of servers (roughly under five to ten) that change infrequently, direct per-server connections in the agent’s own configuration remain simpler and avoid the added hop and the added pass-through logic to get right. Federation earns its complexity once the number of internal servers, or the rate of change among them, makes per-agent configuration maintenance itself a bottleneck.
The next lesson builds the internal registry and catalog that a federation gateway (or any client doing direct connections) uses to know what servers exist in the first place.