Courses/Introduction to Model Context Protocol (MCP)/Building a Client That Drives the Server
Building and TestingLesson 13 of 21

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.

pythonConnecting to a local server over stdio and completing the initialize handshake.
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.

pythonThe two essential client methods, wrapping the session's built-in calls.
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.

bashTest the client in isolation first, then run the complete application.
uv run mcp_client.py    # prints available tools
uv run main.py          # runs the full app with Claude

The 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.

Your Appmain.pyMCP ClientClientSessionServerClaudelist_tools / call_toolapp sends tools+query, gets tool_use, returns result

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