You have seen a dozen tutorials on building MCP servers. But how do you actually connect to one from your own code? If you want your Python app or AI agent to discover and call MCP tools programmatically -- not through Claude Desktop or Cursor -- you need a client. Here is how to build one.
The server (so you can test end-to-end)
Save this as server.py. It exposes a single get_weather tool over stdio:
# server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
@mcp.tool()
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
# Stub response -- swap in a real API call if you want
return f"Sunny, 22C in {city}"
if __name__ == "__main__":
mcp.run(transport="stdio")
That is the server side handled in 10 lines. Now for the interesting part.
The client
Save this as client.py:
# client.py
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
# 1. Point at the server script
server_params = StdioServerParameters(
command="python",
args=["server.py"],
)
# 2. Connect over stdio
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# 3. Handshake
await session.initialize()
# 4. Discover tools
response = await session.list_tools()
print("Available tools:")
for tool in response.tools:
print(f" - {tool.name}: {tool.description}")
# 5. Call a tool
result = await session.call_tool(
"get_weather", arguments={"city": "Tokyo"}
)
print(f"\nResult: {result.content[0].text}")
asyncio.run(main())
Install the SDK and run it:
pip install "mcp[cli]"
python client.py
What the code does
StdioServerParameters tells the client how to launch the server. You pass the command (python) and the args (["server.py"]). The client spawns the server as a subprocess and talks to it over stdin/stdout.
stdio_client opens the transport and gives you a (read, write) pair. These are async streams the ClientSession uses to send and receive JSON-RPC messages under the hood.
session.initialize() performs the MCP handshake. The client and server exchange protocol versions and capabilities. Skip this and every subsequent call will fail.
session.list_tools() returns every tool the server exposes, including its name, description, and input schema. This is how an agent discovers what it can do at runtime.
session.call_tool() invokes a specific tool by name with a dictionary of arguments. The result comes back as a list of content blocks -- text, images, or embedded resources.
Expected output
Available tools:
- get_weather: Get the current weather for a city.
Result: Sunny, 22C in Tokyo
Where to go from here
This client is deliberately minimal. In production, you would:
-
Swap stdio for Streamable HTTP to connect to remote servers. Replace
stdio_clientwith an HTTP transport and point it athttp://your-server:8000/mcp. -
Feed discovered tools to an LLM so it can decide which tools to call based on user queries. The
tool.inputSchemafromlist_tools()maps directly to the function-calling format most LLM APIs expect. -
Handle errors by wrapping
call_toolin try/except. MCP defines structured error responses you can parse and surface to users.
If you are building agents that need to manage dozens of MCP tools without drowning in context, check out our earlier post on MCP Tool Overload -- it covers strategies for dynamic tool selection that pair well with the client pattern above.
Part of the AI Agent Quick Tips series.
Top comments (0)