Building an MCP Client
Up to this point, you have relied on the “MCP Inspector” or “Claude Desktop” to act as the Host. These applications provide a polished User Interface (UI) to interact with your server.
However, there are times when you do not want a chat interface. You might want to build an automated script, a background worker, or a custom application that leverages MCP capabilities without a human in the loop. To do this, you need to interact with the server programmatically.
In this lesson, you will build a custom MCP Client. This gives you the flexibility to call Tools, read Resources, and fetch Prompts directly from your code.
Project Preparation
Before you write any code, you need to set up a clean workspace. This ensures your server and client logic remain isolated from previous lessons.
Open your terminal and create a new directory for this lesson:
$ mkdir lesson_3
$ cd lesson_3
Initialize a new project using uv. This creates the necessary project structure and a virtual environment:
$ uv init
Next, install the MCP SDK. You will use the [cli] extra to ensure you have all command-line utilities available, though you will primarily import the library in your Python scripts:
$ uv add "mcp[cli]"
Now that your environment is ready, you can start building the application.
The Unified Server
In previous lessons, you wrote separate servers for Tools, Resources, and Prompts. To test a client effectively, it helps to have a single server that implements every capability the protocol offers.
You will create a “Travel Planner” server that combines:
- Resources: To fetch travel alerts.
- Tools: To calculate budgets.
- Prompts: To generate a travel plan context.
Create a file named complete_mcp_server.py and paste the following code:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Travel Planner")
TRAVEL_ALERTS = {
"london": "Tube strike scheduled for Friday. Expect delays.",
"paris": "Metro Line 4 under maintenance. Replacement buses active.",
"tokyo": "Typhoon season approaching. Check weather daily.",
"new york": "Subway running on holiday schedule.",
}
@mcp.resource("travel://alerts/{city}")
def get_travel_alerts(city: str) -> str:
"""
Returns current travel alerts or warnings for a specific city.
"""
city_lower = city.lower()
return TRAVEL_ALERTS.get(city_lower, "No active alerts reported for this city.")
@mcp.resource("travel://destinations/list")
def list_supported_destinations() -> str:
"""Returns a list of cities we have specific monitoring for."""
return ", ".join([city.title() for city in TRAVEL_ALERTS.keys()])
@mcp.tool()
def calculate_trip_budget(days: int, travelers: int, daily_spend: float, currency_rate: float = 1.0) -> str:
"""
Calculates the total estimated budget for a trip.
"""
if days < 1 or travelers < 1:
return "Error: Days and travelers must be at least 1."
base_total = days * travelers * daily_spend
converted_total = base_total * currency_rate
return (
f"Budget Estimate:\n"
f"- Travelers: {travelers}\n"
f"- Duration: {days} days\n"
f"- Daily avg: {daily_spend}\n"
f"-------------------------\n"
f"TOTAL: {converted_total:.2f} (at rate {currency_rate})"
)
@mcp.prompt()
def draft_travel_plan(destination: str, days: int, travelers: int) -> str:
"""
Creates a prompt with PRE-LOADED resource data.
The LLM receives the alerts as context immediately, so it doesn't need to fetch them.
"""
city_lower = destination.lower()
current_alerts = TRAVEL_ALERTS.get(city_lower, "No active alerts reported for this city.")
return f"""
I would like to plan a trip to {destination} for {days} days for {travelers} people.
=== CONTEXT: TRAVEL ALERTS ===
Current status for {destination}: "{current_alerts}"
==============================
Please perform the following steps:
1. Acknowledge the travel alerts provided above in the context.
2. Use the 'calculate_trip_budget' tool to estimate costs (assume $150 per person/day).
3. Draft a daily itinerary. If the alerts mentioned disruptions (like strikes or maintenance), adjust the itinerary to avoid those transport methods.
Please present the final response as a structured travel guide.
"""
if __name__ == "__main__":
mcp.run(transport="streamable-http")
Understanding the Unified Server
This server intentionally bundles Resources, Tools, and Prompts so your client can exercise the full MCP surface area in one place:
-
FastMCP setup:
FastMCP("Travel Planner")registers a named server and builds a lightweight runtime. -
Resource endpoints:
-
travel://alerts/{city}returns a single alert string for a city. -
travel://destinations/listreturns the list of available cities.
-
-
Tool logic:
calculate_trip_budgetvalidates inputs and returns a formatted budget summary. -
Prompt composition:
draft_travel_planpreloads alerts into a ready-to-send prompt so an LLM receives context without an extra resource call. -
HTTP transport:
mcp.run(transport="streamable-http")exposes the server over HTTP so clients can connect programmatically.
The Client Script
Now you will write the software to consume this server.
When you use the mcp library in Python, you gain access to ClientSession. This object acts as your API to the server. Instead of clicking buttons in a UI, you use methods like read_resource(), call_tool(), and get_prompt() to drive the interaction.
Create a file named mcp_client.py and paste in the following code:
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from mcp.types import TextContent
SERVER_URL = "http://localhost:8000/mcp"
async def run_client():
print(f"🔌 Connecting to server at {SERVER_URL}...")
async with streamablehttp_client(SERVER_URL) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
print("✅ Connected and Initialized!\n")
print("--- 1. Testing Resource (Travel Alerts) ---")
resources = await session.list_resources()
print(f"Found {len(resources.resources)} available resources.")
resource_uri = "travel://alerts/london"
print(f"Reading: {resource_uri}")
res_result = await session.read_resource(resource_uri)
for content in res_result.contents:
if hasattr(content, "text"):
print(f"📜 CONTENT: {content.text}")
print("")
print("--- 2. Testing Tool (Budget Calculator) ---")
tool_name = "calculate_trip_budget"
tool_args = {
"days": 5,
"travelers": 2,
"daily_spend": 150,
"currency_rate": 0.85
}
print(f"Calling tool '{tool_name}' with args: {tool_args}")
tool_result = await session.call_tool(tool_name, arguments=tool_args)
for content in tool_result.content:
if isinstance(content, TextContent):
print(f"🛠️ RESULT:\n{content.text}")
print("")
print("--- 3. Testing Prompt (Draft Travel Plan) ---")
prompt_name = "draft_travel_plan"
prompt_args = {
"destination": "London",
"days": "5",
"travelers": "2"
}
print(f"Fetching prompt '{prompt_name}' with args: {prompt_args}")
prompt_result = await session.get_prompt(prompt_name, arguments=prompt_args)
for message in prompt_result.messages:
print(f"🤖 ROLE: {message.role}")
if hasattr(message.content, "text"):
print(f"📝 MESSAGE CONTENT:\n{message.content.text}")
else:
print(f"📝 MESSAGE CONTENT:\n{message.content}")
if __name__ == "__main__":
asyncio.run(run_client())
Understanding the Client Script
This client discovers capabilities at runtime and then invokes each MCP feature directly:
-
Connect and initialize:
streamablehttp_client()opens the connection;session.initialize()completes the MCP handshake. -
List resources:
list_resources()verifies what the server exposes before reading a specific URI. -
Read a resource:
read_resource("travel://alerts/london")fetches alert data and prints any text payloads. -
Call a tool:
call_tool("calculate_trip_budget", ...)sends arguments and prints tool output. -
Fetch a prompt:
get_prompt("draft_travel_plan", ...)retrieves the assembled prompt messages.
Running the Demo
You have now written two complete pieces of software: a feature-rich MCP Server and a custom Client to control it.
In the next video section, you will run these scripts to verify the connection. You will see how the client discovers capabilities, executes code on the server, and receives structured data back.