Multi-Server Orchestration: Agents Calling Many MCP Servers at Once

13 min read Module 8 of 10 Topic 24 of 30

What you'll learn

  • Manage concurrent MCP client sessions to multiple servers within one agent
  • Merge tool catalogs from many servers while preserving namespacing from Module 6
  • Handle partial failure: one server down should not block tools from healthy servers
  • Manage context-window cost as the aggregated tool catalog grows across many connected servers
Building this at your company? For enterprise and company teams taking this to production: book a 30-minute session with our AI engineers for architecture guidance, code review, and a rollout plan for your use case.
Book a Team Session

An agent tackling a real enterprise task often needs tools from several systems in one reasoning session, look up a customer in the CRM, check their open tickets, search the knowledge base for a relevant policy. This lesson builds the session-management layer for holding several MCP connections concurrently within the agent itself, as an alternative to routing everything through a dedicated federation gateway (Lesson 17), and addresses a cost that grows specifically with this pattern: the context-window and tool-selection burden of a large aggregated catalog.

Managing Concurrent Sessions

from mcp.client.session import ClientSession
import asyncio

class MultiServerAgentSession:
    def __init__(self, server_configs: dict[str, dict]):
        self.server_configs = server_configs  # {"crm": {...}, "kb": {...}, "support": {...}}
        self.sessions: dict[str, ClientSession] = {}
        self.unavailable: set[str] = set()

    async def connect_all(self):
        """Connect to every configured server concurrently; isolate failures per server."""
        results = await asyncio.gather(
            *(self._connect_one(name, cfg) for name, cfg in self.server_configs.items()),
            return_exceptions=True,
        )
        for name, result in zip(self.server_configs, results):
            if isinstance(result, Exception):
                self.unavailable.add(name)
                logging.warning(f"MCP server '{name}' unavailable at session start: {result}")

    async def _connect_one(self, name: str, cfg: dict):
        session = await ClientSession.connect(cfg["url"], headers=cfg.get("headers", {}))
        self.sessions[name] = session

Merging Tool Catalogs with Namespacing

    async def get_merged_tools(self) -> list[dict]:
        """Aggregate tools from all currently-healthy servers, namespaced by server name."""
        all_tools = []
        for name, session in self.sessions.items():
            if name in self.unavailable:
                continue
            try:
                tools = await session.list_tools()
                for t in tools:
                    t.name = f"{name}.{t.name}"  # Same convention as Lesson 17's federation gateway
                all_tools.extend(tools)
            except Exception:
                self.unavailable.add(name)  # Mark unhealthy mid-session, not just at connect time
        return all_tools

    async def call_tool(self, namespaced_name: str, arguments: dict) -> dict:
        server_name, _, tool_name = namespaced_name.partition(".")
        if server_name in self.unavailable:
            return {"isError": True, "message": f"Server '{server_name}' is currently unavailable"}
        return await self.sessions[server_name].call_tool(tool_name, arguments)

Graceful Degradation Under Partial Failure

flowchart TD
    Start["Agent session starts"] --> Connect["Connect to CRM, KB, Support\nconcurrently"]
    Connect --> Check{"KB server\nreachable?"}
    Check -->|no| Degrade["Exclude kb.* tools from\nmerged catalog, mark unavailable"]
    Check -->|yes| Full["Full catalog: crm.*, kb.*, support.*"]
    Degrade --> Continue["Agent proceeds with\ncrm.* and support.* tools only"]
    Full --> Continue2["Agent proceeds with\nfull tool catalog"]
    Continue --> Retry["Periodic reconnect attempt\nto KB in background"]

    style Start fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style Check fill:#fff7ed,stroke:#f59e0b,color:#0F172A
    style Degrade fill:#fef2f2,stroke:#dc2626,color:#0F172A
    style Full fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style Continue fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style Continue2 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
async def background_reconnect_loop(session_mgr: MultiServerAgentSession, interval: int = 30):
    """Periodically retry unavailable servers so a transient outage self-heals
    without requiring the agent session to be restarted."""
    while True:
        await asyncio.sleep(interval)
        for name in list(session_mgr.unavailable):
            try:
                await session_mgr._connect_one(name, session_mgr.server_configs[name])
                session_mgr.unavailable.discard(name)
                logging.info(f"MCP server '{name}' reconnected")
            except Exception:
                pass  # Still down, try again next interval

Managing Catalog Size: When More Connected Servers Stops Helping

As an agent connects to more servers, whether directly (this lesson) or via federation (Lesson 17), the aggregated tool catalog grows correspondingly, and it is worth being deliberate about the point at which this stops being a straightforward win. Beyond raw context-window token cost (every tool’s name, description, and schema occupies tokens on every turn the model considers its options), a large flat catalog increases the model’s tool-selection search space directly: more tools that are superficially similar to each other (multiple search_* variants across different servers, for instance) raises the odds of the model picking a plausible-but-wrong one, the same underlying failure mode Lesson 5 described for naming collisions within a single server, just occurring at the scale of an entire aggregated multi-server catalog.

flowchart LR
    Servers["15 connected MCP servers"] --> Catalog["Aggregated catalog:\n400 tools"]
    Catalog --> Cost1["Token cost: every turn\ncarries all 400 tool schemas"]
    Catalog --> Cost2["Selection cost: more near-duplicate\noptions increases wrong-tool-picked rate"]

    style Servers fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style Catalog fill:#fff7ed,stroke:#f59e0b,color:#0F172A
    style Cost1 fill:#fef2f2,stroke:#dc2626,color:#0F172A
    style Cost2 fill:#fef2f2,stroke:#dc2626,color:#0F172A

The practical mitigation is relevance scoping: connect a given agent session only to the servers genuinely relevant to its role or the task it was launched for, rather than defaulting every agent to maximal connectivity across the entire organization’s server fleet just because it is technically possible.

# Scope connections per agent role, rather than connecting every agent
# to every server that exists in the organization by default.
AGENT_ROLE_SERVERS: dict[str, list[str]] = {
    "sales_agent": ["crm", "kb"],
    "support_agent": ["kb", "support", "crm"],
    "ops_agent": ["warehouse", "crm"],
}

def build_session_for_role(role: str) -> MultiServerAgentSession:
    relevant_servers = {name: ALL_SERVER_CONFIGS[name] for name in AGENT_ROLE_SERVERS[role]}
    return MultiServerAgentSession(relevant_servers)

For cases where a broader catalog genuinely is needed but selection accuracy has degraded, a second-tier mitigation is a two-stage tool selection pattern (a lightweight preliminary step that narrows the full catalog down to a small relevant subset before the main reasoning turn sees any tool schemas at all), though this adds its own latency and complexity and is worth reaching for only once relevance scoping alone has been tried and found insufficient for a genuinely broad-scope agent.

This closes Module 8. Module 9 turns to making all of this observable and testable in production: distributed tracing across these multi-server calls, contract testing for individual tools, and patterns for tools whose execution takes longer than a single request-response cycle.

Knowledge Check

3 questions to test your understanding

1 An agent holds connections to a CRM MCP server, a knowledge-base MCP server, and a support-ticketing MCP server. The knowledge-base server is temporarily down. What should happen to the agent's overall tool availability?

2 Why does merging tool catalogs from multiple MCP servers require the namespacing discipline established in Module 6, even for an agent that only connects to two or three servers directly (without a federation gateway)?

3 An agent connects to 15 MCP servers simultaneously, resulting in an aggregated catalog of 400 tools presented to the model on every turn. What problem does this create beyond raw context-window token cost, and what is a reasonable mitigation?

Go further with expert guidance

Ready to build production AI?
Talk to our R&D team.

These courses give you the foundation. Our embedded AI teams take you from prototype to production in 30–90 days, with your team, your codebase, your goals. Book a free strategy call to see how we can accelerate your AI initiative.

30 minutes · No obligation · Expert AI engineers, not sales reps

AI Architecture Review

Audit your current stack and identify high-impact improvements

Project Review

Get expert feedback on your AI implementation and codebase

Team Mentoring

Upskill your engineers with hands-on AI coaching sessions

AI Strategy

Define your AI roadmap, prioritization, and implementation plan