Building a Client That Drives the Server
The Client's Job
With a tested server in hand, we build the client side — the code that lets your application communicate with the server and access its functionality. In most real projects you implement either a client or a server, not both; we build both here only so you can see how they fit together.
The client has two layers. The Client Session is the actual connection to the server, provided by the MCP SDK. On top of it we write a small wrapper class that makes the session easier to use and — importantly — manages cleanup, so connections are always closed properly when we're done.
Where the client plugs into the flow
Recall the end-to-end request flow: the client is what your app uses at two moments — to get the list of tools to send to Claude, and to execute a tool when Claude requests one. Everything the client does serves those two jobs.
Connecting to the Server
For a local stdio server, you connect with stdio_client and wrap the streams in a ClientSession, then call initialize() to perform the handshake. The async context managers ensure the subprocess and connection are torn down cleanly.
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
server_params = StdioServerParameters(command="uv", args=["run", "mcp_server.py"])
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# ... use the session here ...Listing and Calling Tools
The two core operations your app needs are list_tools() and call_tool(). We wrap them in our client class so the rest of the app doesn't touch the session directly. list_tools fetches the definitions to send to Claude; call_tool runs a specific tool with the arguments Claude chose.
import mcp.types as types
async def list_tools(self) -> list[types.Tool]:
result = await self.session().list_tools()
return result.tools
async def call_tool(
self, tool_name: str, tool_input: dict
) -> types.CallToolResult | None:
return await self.session().call_tool(tool_name, tool_input)You can sanity-check the client on its own before involving Claude. A small test harness that connects and prints the tools confirms the wiring: run it and you should see your tool definitions, including descriptions and input schemas.
uv run mcp_client.py # prints available tools
uv run main.py # runs the full app with ClaudeThe Complete Loop, in Code
Putting it together: when a user asks 'What is the contents of the report.pdf document?', the application uses the client to get the available tools, sends them with the question to Claude, Claude decides to use read_doc_contents, the app uses the client to execute it, and the result goes back to Claude to compose the final answer. This is the eleven-step flow from earlier — now you can see exactly which lines do which step.
The app uses the client (list_tools/call_tool) to reach the server, and talks to Claude directly — the client never calls Claude.
Next
Our client can list and call tools. Next we extend it to read resources — so the app can inject document contents directly into prompts without a tool call.
Key Takeaways
- ✓The client lets your application communicate with an MCP server; in practice you usually build either a client or a server, not both.
- ✓It has two layers: the SDK-provided ClientSession (the connection) and a small wrapper class you write to simplify use and guarantee cleanup.
- ✓Connect to a local server with stdio_client + ClientSession, then call session.initialize() to perform the handshake (async context managers handle teardown).
- ✓The two core operations are list_tools() (fetch definitions for Claude) and call_tool(name, input) (execute the tool Claude chose).
- ✓Test the client in isolation first (print the tools) before running the full app with Claude.
- ✓In the full loop the app uses the client to reach the server and talks to Claude separately — the client never calls Claude itself.
Check Your Understanding
Test what you learned in this lesson.
Q1.What are the two layers of the MCP client described here?
Q2.Which two client methods cover the core needs of the request flow?
Q3.How does a client connect to a local stdio server before using it?
Q4.In the complete loop, what is the client's relationship to Claude?
Practice This Lesson