Skip to main content

Connecting an Agent to MCP Servers

Introduction

You built a server (L140). Now flip to the other side: how does your own agent — not Claude Desktop, your codeuse an MCP server? This is the client side, and it's where MCP finally meets everything from Section 3: the agent loop.

An MCP client's whole job is a bridge: take a server's tools, hand them to your LLM as regular tools, and when the model calls one, route the call to the right server and feed the result back. MCP servers become tools in your agent loop — automatically.

The payoff is the M+N promise from L137 (The Integration Problem MCP Solves), made real: connect your agent to the GitHub server, the Postgres server, the Slack server — and your agent can now do all of it, without you writing a single integration. We'll build the client from scratch (to understand the bridge), then use the one-line MCP connector and frameworks for production.

In this lesson:

  • The client's job — bridging a server's tools into your agent loop (hands-on)
  • Build a client from scratchClientSession, list_tools, call_tool, wired into Claude
  • Many servers at once — aggregating tools and routing calls
  • The easy paths — the MCP connector and frameworks, and when to use which

Scope: this is connecting/consuming servers. The broader ecosystem & the security risks of plugging into third-party servers close the section (L142 — The MCP Ecosystem & Security Considerations).

An infographic titled 'Connecting an Agent to MCP Servers' showing the client side of the Model Context Protocol. On the left is YOUR AGENT, a host containing the LLM and a unified TOOLSET. On the right are three MCP servers — github, postgres, and slack — each exposing its own tools. Two flows connect them. First, AGGREGATE: the agent opens one client per server, calls list_tools on each, and merges every server's tools into one flat toolset that the model sees; the model just picks a tool by name and does not know which server owns it. Second, ROUTE: when the model emits a tool_use for a tool such as create_pr, the host knows that tool belongs to the github server, so it routes the call_tool request there, runs it on the server, and feeds the tool_result back into the loop. This is MCP meeting the agent loop from the tools section: discover, give the tools to the model, the model picks, route the call, return the result, repeat. There are two ways to build it. The do-it-yourself client uses the MCP SDK directly — ClientSession with the stdio transport, session.initialize, session.list_tools, and session.call_tool — wired into the Anthropic Messages API, best for local stdio servers and full control. The easier path is the MCP connector built into the Claude Messages API: pass an mcp_servers array of remote server URLs and a matching mcp_toolset, with the beta header, and Claude discovers and calls the servers' tools for you with no client to manage, for remote HTTPS servers and tools only. The three cards summarize it: aggregate every server's tools into one set, route each call back to the server that owns it, and choose between the DIY client and the MCP connector. The takeaway: connecting an agent means discovering every server's tools into one toolset, letting the model pick, and routing each call back to its server, so your agent speaks one protocol and gains every server's powers — the M+N payoff in action. In practice you rarely hand-roll the client: frameworks such as the Claude Agent SDK and the LangChain and LangGraph MCP adapters point at MCP servers and auto-wire their tools into an agent, handling discovery, conversion, routing, and the loop, but the four steps underneath are exactly what this lesson teaches, which is what you need to debug them when a tool fails to appear or a call mis-routes.

The Client's Job — Bridge MCP to the Agent Loop

Strip it down and an MCP client does four things, every one of which you already know from Section 3's tool-use loop — MCP just sources the tools from a server instead of your own code:

  1. Connect — open a session to the server (one client per server) and run the initialize handshake (L138).
  2. Discover — call tools/list to get the server's tools, and convert them to your LLM's tool format.
  3. Ask — pass those tools to the model; it decides which to call (model-controlled, L139 — Tools, Resources & Prompts in MCP).
  4. Route — on a tool_use, call tools/call on the server, get the result, feed it back, and loop.

The magic is in step 4: the model picks a tool by name, and the client routes that call to the server that owns it — which is what makes connecting to many servers trivial. Play with the routing: pick a request and watch the model choose a tool and the host send it to the right server:

Your agent is connected to three MCP servers — github, postgres, slack. Their tools all aggregate into ONE flat toolset the model sees (it just picks a tool by name; it has no idea which server owns it). Pick a request and watch: “open a PR” → the model emits tool_use: create_pr → the HOST knows create_pr belongs to the github server → it routes call_tool() there → ✓ PR #128 opened. “How many users signed up?” routes to postgres (run_query); “tell the team” routes to slack (send_message). The client-side core in one view: aggregate every server's tools into one set, let the model choose, and route each call back to its owner — connect a 4th server and its tools simply appear. That's the M+N payoff: one protocol, every server's powers.

Notice what the model sees: just a flat list of tools — it has no idea create_pr lives on a "GitHub server" and run_query on a "Postgres server." The host keeps that mapping and routes each call. That separation is the whole trick: to the model it's one toolbox; to you it's many servers.

Build a Client From Scratch

Here's the bridge in real code — the official Python MCP SDK (mcp) plus the Anthropic SDK, the exact four steps above. Read it once, then we'll trace it:

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from anthropic import Anthropic
anthropic = Anthropic()

# 1) CONNECT — one ClientSession per server (runs the L138 initialize handshake)
params = StdioServerParameters(command="python", args=["server.py"])
async with stdio_client(params) as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()

        # 2) DISCOVER — list the server's tools, convert to Claude's tool format
        resp = await session.list_tools()
        tools = [{"name": t.name, "description": t.description,
                  "input_schema": t.inputSchema} for t in resp.tools]

        # 3) ASK — give the tools to the model; it picks one
        msg = anthropic.messages.create(model="claude-opus-4-8", max_tokens=1024,
                                        messages=messages, tools=tools)

        # 4) ROUTE — on a tool_use, call the tool ON THE SERVER, feed the result back
        for block in msg.content:
            if block.type == "tool_use":
                result = await session.call_tool(block.name, block.input)   # runs on the server
                messages += [
                    {"role": "assistant", "content": msg.content},
                    {"role": "user", "content": [{"type": "tool_result",
                        "tool_use_id": block.id, "content": result.content}]},
                ]
                msg = anthropic.messages.create(model="claude-opus-4-8",      # …and loop
                                 max_tokens=1024, messages=messages, tools=tools)

Trace the four steps: stdio_client + ClientSession + initialize is connect; session.list_tools() + the comprehension is discover & convert (note the field rename — MCP's inputSchema → Claude's input_schema); messages.create(..., tools=tools) is ask; and session.call_tool(block.name, block.input) is route — it runs the function on the server and you stitch the tool_result back into messages before looping. That's it: a server's tools are now your agent's tools. (The SDKs even ship helpers — anthropic[mcp]'s mcp_tools() — to skip the manual conversion.)

Many Servers — Aggregate & Route

One server is useful; the real power is many. Connecting to several servers is just looping the same four steps — and it's how M+N pays off:

  • Aggregate. Open a client to each server, call list_tools on all of them, and concatenate the results into one flat toolset. The model sees a single toolbox — create_pr, run_query, send_message — with no notion of boundaries.
  • Route. Keep a map of which tool came from which session. When the model calls run_query, you look up its session and dispatch call_tool there. The model never knows; the host owns the routing.

That's the entire trick to a multi-tool agent over MCP: all_tools = sum(list_tools() for each server), and tool → its session → call_tool. Add the Slack server tomorrow and your agent gains Slack's tools the instant you connect — no glue code. (You felt this in the lab: one toolset in, routed calls out.) This is also why good tool names + descriptions matter more as you add servers — with dozens of tools, the model picks by name and description, so clarity drives selection accuracy.

The Easy Path — The MCP Connector

Building the client teaches you the bridge — but for remote servers you often don't need to build anything. Claude's MCP connector lets the Messages API itself connect to MCP servers: you just list them, and Claude handles discovery, calling, and routing. No ClientSession, no loop:

# THE EASY PATH — no client to manage. Let the Claude API connect to REMOTE servers:
anthropic.beta.messages.create(
    model="claude-opus-4-8", max_tokens=1024, messages=messages,
    mcp_servers=[
        {"type": "url", "url": "https://mcp.github.com/sse", "name": "github",
         "authorization_token": GITHUB_OAUTH_TOKEN},
    ],
    tools=[{"type": "mcp_toolset", "mcp_server_name": "github"}],   # enable its tools
    betas=["mcp-client-2025-11-20"],
)
# Claude discovers + calls the server's tools itself. Remote (HTTPS) only · tools only.

Two pieces: an mcp_servers array (each remote server's url, name, and OAuth authorization_token) and a matching mcp_toolset in tools (which enables/allowlists its tools). Add a second server → a second mcp_servers entry + a second mcp_toolset, and Claude composes them automatically. The trade-off vs the DIY client: the connector is remote-only (public HTTPS servers — no local stdio) and tools-only (no resources/prompts). So: connector for hosted servers and speed; build the client for local servers, resources/prompts, or full control over the loop.

DIY vs Connector vs Framework

Three ways to connect an agent, by how much you hand off:

DIY clientMCP connectorFramework
You writethe full loop (ClientSession…)a few lines (mcp_servers)almost nothing
Serverslocal (stdio) + remoteremote (HTTPS) onlyboth
Supportstools, resources, promptstools onlytools (+ more)
Best forlocal servers, full control, learninghosted servers, fastproduction agents

The frameworks wrap all of this: the Claude Agent SDK, LangChain/LangGraph MCP adapters, the OpenAI Agents SDK, and others let you point at MCP servers and auto-wire their tools into an agent — discovery, conversion, routing, and the loop handled. In 2026 that's the common production path: you rarely hand-roll the client. But you now understand what they do — connect, discover, aggregate, route — which is exactly what you need to debug them when a tool doesn't show up or a call mis-routes.

🧪 Try It Yourself

Reason it through (use the router lab to check 1–2):

  1. Your agent is connected to the github and slack servers. The model emits tool_use for send_message. How does the client know where to send it?
  2. What two things must you do to a server's tools between list_tools() and messages.create()?
  3. You need to connect to a colleague's local MCP server running over stdio. Connector or DIY client — and why?
  4. Connecting to a third-party hosted GitHub server, you want the least code. Which path, and which two API pieces?
  5. You add a 4th server with 40 tools and the model starts mis-picking. Two fixes?

(1) The host keeps a map of tool → the session it came from; send_message came from the slack server's session, so it dispatches call_tool there. The model just picked a name. (2) Convert them to the LLM's tool format (MCP inputSchema → Claude input_schema, plus name/description) and pass them in tools=. (3) DIY client — the MCP connector is remote-HTTPS-only; local stdio servers must be driven by a ClientSession. (4) The MCP connector — add an mcp_servers entry (url + name + token) and a matching mcp_toolset in tools, with the beta header. (5) Sharpen tool names/descriptions (selection is by name + description), and allowlist / defer_loading to surface only the relevant tools (or split work across focused agents).

Mental-Model Corrections

  • "I have to hand-code an integration per tool." No — that's the pre-MCP world. A client discovers a server's tools at runtime and converts them automatically; you write the bridge once, not per tool.
  • "The model knows which server a tool is on." It doesn't — the model sees one flat toolset and picks by name. The host holds the tool→session map and routes the call.
  • "MCP is a different agent loop." It's the same Section-3 tool-use loop; MCP just sources the tools from a server (list_toolstools= → on tool_usecall_tool).
  • "The MCP connector replaces the client entirely." Only for remote, tools-only use. Local (stdio) servers and resources/prompts still need a real client (or helpers).
  • "Connecting to many servers is hard." It's the same four steps in a loop: aggregate their list_tools, route by origin. Adding a server adds its tools for free.
  • "I should always build the client myself." In production, frameworks (Claude Agent SDK, LangChain MCP adapters) wire MCP servers in for you — build-it-yourself is for learning, local servers, or full control.

Key Takeaways

  • A client bridges a server's tools into your agent loop — the same 4 steps as Section 3: connect (ClientSession + initialize) → discover (list_tools, convert inputSchemainput_schema) → ask (messages.create(tools=…)) → route (call_tool on the server, feed the result back, loop).
  • The model sees one flat toolset; the host routes. It picks a tool by name; the client maps tool → session and dispatches call_tool to the owner — the model never knows there are multiple servers.
  • Many servers = aggregate + route. Concatenate every server's list_tools into one toolset; add a server → its tools appear instantly. That's M+N in code. (Good tool names/descriptions drive selection as the set grows.)
  • The MCP connector is the one-line path for remote servers: mcp_servers (url + name + OAuth token) + mcp_toolset in tools, beta mcp-client-2025-11-20. HTTPS-only, tools-only — no client to manage.
  • Pick the path: DIY client (local/stdio, resources/prompts, control) · connector (remote, fast, tools-only) · framework (production — Claude Agent SDK, LangChain/LangGraph MCP adapters auto-wire it).
  • Next — L142: The MCP Ecosystem & Security Considerations — the thousands of servers out there, and the real risks of plugging your agent into them.