3Build the client

Wire the client to your server

4 min read792 words

The server is done. Now you need the other half of the round-trip: the MCP client that lets your application code discover and execute the tools the server exposes. Most real projects implement a client or a server. You are going to implement both in this one codebase so the full picture stays in one place.

The client's two components

Open mcp_client.py. The client is a custom class that wraps around a client session — the actual connection to the MCP server, provided by the MCP Python SDK. Why wrap it? Because the session owns real resources: a subprocess, open pipes, and async context that all need to be torn down cleanly. Your wrapper class handles the connect / cleanup / __aenter__ / __aexit__ plumbing so the rest of your application never has to think about it.

Once wrapped, the client's job in the CLI chatbot is narrow and well-defined. It is called at exactly two points in the application flow: once to ask the server what tools are available (so they can be sent to Claude), and again to execute a specific tool when Claude requests one.

Implementing list_tools

First, the tool discovery function. This is the one the chat loop calls before each turn to get the list of tools Claude should consider:

async def list_tools(self) -> list[types.Tool]:
    result = await self.session().list_tools()
    return result.tools

Straightforward. You call the session's built-in list_tools(), which sends a ListToolsRequest over the transport to the server and gets back a ListToolsResult. You return the .tools attribute — a list of types.Tool objects, each with the name, description, and input schema of one registered tool.

Implementing call_tool

The execution function is equally thin:

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 take the tool name and the argument dictionary Claude produced, and pass them straight through to the session's call_tool. The SDK handles the CallToolRequest / CallToolResult exchange with the server and returns the result.

Both functions are small on purpose. The client is plumbing — it should not contain business logic. Anything interesting belongs on the server (in the tool implementation) or in the chat loop (in how the result is handed back to Claude).

Testing the client in isolation

The mcp_client.py file ships with a small test harness at the bottom so you can run the client directly and verify it connects before touching the chat loop:

uv run mcp_client.py

This will spawn your MCP server, connect to it, and print the list of available tools. You should see both read_doc_contents and edit_document show up with their descriptions and input schemas. If they do not, the problem is the client wiring — fix it here before you try to debug it through a full Claude conversation.

End-to-end verification

Once list_tools and call_tool are in place, the full chatbot can run:

uv run main.py

Try asking: "What is the contents of the report.pdf document?"

Here is the trace of what happens — the same 12-step flow from Module 1, now running on your code:

  1. Your application calls client.list_tools() to get the tool list.
  2. The tools are bundled with your question and sent to Claude.
  3. Claude decides it needs read_doc_contents and emits a tool call with doc_id="report.pdf".
  4. Your application calls client.call_tool("read_doc_contents", {"doc_id": "report.pdf"}).
  5. The MCP client forwards the request to the server; the server runs the tool and returns the content.
  6. The result goes back to Claude, which uses it to produce a final answer.

Test-first matters here. Prove the client can list tools before you try to run the full loop. Every time something breaks in the rest of the course, your first debugging step should be re-running mcp_client.py in isolation.

Key Takeaways

  • 1The MCP client is a custom class that wraps the SDK's client session, centralising resource cleanup so application code never has to manage it directly.
  • 2list_tools and call_tool are thin pass-throughs to the session, delegating the ListToolsRequest and CallToolRequest exchanges to the SDK.
  • 3The client is plumbing — keep it free of business logic so the server and the chat loop remain the places where interesting things happen.
  • 4Always test the client in isolation with its built-in harness before running the full chat loop; it is the fastest way to catch wiring bugs.