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 code — use 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 scratch —
ClientSession,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).

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:
- Connect — open a session to the server (one client per server) and run the initialize handshake (L138).
- Discover — call
tools/listto get the server's tools, and convert them to your LLM's tool format. - Ask — pass those tools to the model; it decides which to call (model-controlled, L139 — Tools, Resources & Prompts in MCP).
- Route — on a
tool_use, calltools/callon 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:

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_toolson 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 dispatchcall_toolthere. 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 client | MCP connector | Framework | |
|---|---|---|---|
| You write | the full loop (ClientSession…) | a few lines (mcp_servers) | almost nothing |
| Servers | local (stdio) + remote | remote (HTTPS) only | both |
| Supports | tools, resources, prompts | tools only | tools (+ more) |
| Best for | local servers, full control, learning | hosted servers, fast | production 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):
- Your agent is connected to the github and slack servers. The model emits
tool_useforsend_message. How does the client know where to send it? - What two things must you do to a server's tools between
list_tools()andmessages.create()? - You need to connect to a colleague's local MCP server running over stdio. Connector or DIY client — and why?
- Connecting to a third-party hosted GitHub server, you want the least code. Which path, and which two API pieces?
- 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_tools→tools=→ ontool_use→call_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, convertinputSchema→input_schema) → ask (messages.create(tools=…)) → route (call_toolon 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_toolto the owner — the model never knows there are multiple servers. - Many servers = aggregate + route. Concatenate every server's
list_toolsinto 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_toolsetintools, betamcp-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.