This commit is contained in:
Zachary Lee 2024-12-10 18:01:54 -05:00
commit 5938c8150e
No known key found for this signature in database
3 changed files with 1086 additions and 454 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

View file

@ -5,15 +5,6 @@ description: "Get started with building your first MCP server and connecting it
In this tutorial, we'll build a simple MCP weather server and connect it to a host, Claude for Desktop. We'll start with a basic setup, and then progress to more complex use cases.
### What is MCP?
MCP is an open protocol that standardizes how applications provide context to LLMs. Think of it as the "Language Server Protocol for AI" - it separates the concerns of providing context from the LLM interaction itself. This separation enables:
1. **Modularity**: Each MCP server focuses on one specific integration or capability
2. **Reusability**: The same server can be used with any MCP-compatible client
3. **Composability**: Multiple servers can be combined to create powerful workflows
4. **Standardization**: A common protocol for all integrations reduces complexity
### What we'll be building
Many LLMs (including Claude) do not currently have the ability to fetch the forecast and severe weather alerts. Let's use MCP to solve that!
@ -45,20 +36,21 @@ MCP servers can provide three main types of capabilities:
This tutorial focuses on tools, but we have intermediate tutorials if you'd like to learn more about Resources and Prompts.
<Tabs>
<Tab title='Python'>
### Prerequisite knowledge
This quickstart assumes you have familiarity with:
- Python or TypeScript
- Python
- LLMs like Claude
### System requirements
For Python, make sure you have Python 3.9 or higher installed. For TypeScript, make sure you have the latest version of Node installed.
For Python, make sure you have Python 3.9 or higher installed.
### Set up your environment
<Tabs>
<Tab title="Python">
First, let's install `uv` and set up our Python project and environment:
<CodeGroup>
@ -75,22 +67,8 @@ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | ie
Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
</Tab>
<Tab title="Node">
First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
Verify your Node.js installation:
```bash
node --version
npm --version
```
For this tutorial, you'll need Node.js version 16 or higher.
</Tab>
</Tabs>
Now, let's create and set up our project:
<Tabs>
<Tab title='Python'>
<CodeGroup>
```bash MacOS/Linux
# Create a new directory for our project
@ -162,9 +140,354 @@ def main():
# Optionally expose other important items at package level
__all__ = ['main', 'server']
```
</Tab>
Now let's dive into building your server.
## Building your server
### Importing packages
Add these to the top of your `server.py`:
```python
from typing import Any
import asyncio
import httpx
from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
import mcp.server.stdio
```
### Setting up the instance
Then initialize the server instance and the base URL for the NWS API:
```python
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
server = Server("weather")
```
### Implementing tool listing
We need to tell clients what tools are available. The `list_tools()` decorator registers this handler:
```python
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
List available tools.
Each tool specifies its arguments using JSON Schema validation.
"""
return [
types.Tool(
name="get-alerts",
description="Get weather alerts for a state",
inputSchema={
"type": "object",
"properties": {
"state": {
"type": "string",
"description": "Two-letter state code (e.g. CA, NY)",
},
},
"required": ["state"],
},
),
types.Tool(
name="get-forecast",
description="Get weather forecast for a location",
inputSchema={
"type": "object",
"properties": {
"latitude": {
"type": "number",
"description": "Latitude of the location",
},
"longitude": {
"type": "number",
"description": "Longitude of the location",
},
},
"required": ["latitude", "longitude"],
},
),
]
```
This defines our two tools: `get-alerts` and `get-forecast`.
### Helper functions
Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
```python
async def make_nws_request(client: httpx.AsyncClient, url: str) -> dict[str, Any] | None:
"""Make a request to the NWS API with proper error handling."""
headers = {
"User-Agent": USER_AGENT,
"Accept": "application/geo+json"
}
try:
response = await client.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
def format_alert(feature: dict) -> str:
"""Format an alert feature into a concise string."""
props = feature["properties"]
return (
f"Event: {props.get('event', 'Unknown')}\n"
f"Area: {props.get('areaDesc', 'Unknown')}\n"
f"Severity: {props.get('severity', 'Unknown')}\n"
f"Status: {props.get('status', 'Unknown')}\n"
f"Headline: {props.get('headline', 'No headline')}\n"
"---"
)
```
### Implementing tool execution
The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
```python
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
Handle tool execution requests.
Tools can fetch weather data and notify clients of changes.
"""
if not arguments:
raise ValueError("Missing arguments")
if name == "get-alerts":
state = arguments.get("state")
if not state:
raise ValueError("Missing state parameter")
# Convert state to uppercase to ensure consistent format
state = state.upper()
if len(state) != 2:
raise ValueError("State must be a two-letter code (e.g. CA, NY)")
async with httpx.AsyncClient() as client:
alerts_url = f"{NWS_API_BASE}/alerts?area={state}"
alerts_data = await make_nws_request(client, alerts_url)
if not alerts_data:
return [types.TextContent(type="text", text="Failed to retrieve alerts data")]
features = alerts_data.get("features", [])
if not features:
return [types.TextContent(type="text", text=f"No active alerts for {state}")]
# Format each alert into a concise string
formatted_alerts = [format_alert(feature) for feature in features[:20]] # only take the first 20 alerts
alerts_text = f"Active alerts for {state}:\n\n" + "\n".join(formatted_alerts)
return [
types.TextContent(
type="text",
text=alerts_text
)
]
elif name == "get-forecast":
try:
latitude = float(arguments.get("latitude"))
longitude = float(arguments.get("longitude"))
except (TypeError, ValueError):
return [types.TextContent(
type="text",
text="Invalid coordinates. Please provide valid numbers for latitude and longitude."
)]
# Basic coordinate validation
if not (-90 <= latitude <= 90) or not (-180 <= longitude <= 180):
return [types.TextContent(
type="text",
text="Invalid coordinates. Latitude must be between -90 and 90, longitude between -180 and 180."
)]
async with httpx.AsyncClient() as client:
# First get the grid point
lat_str = f"{latitude}"
lon_str = f"{longitude}"
points_url = f"{NWS_API_BASE}/points/{lat_str},{lon_str}"
points_data = await make_nws_request(client, points_url)
if not points_data:
return [types.TextContent(type="text", text=f"Failed to retrieve grid point data for coordinates: {latitude}, {longitude}. This location may not be supported by the NWS API (only US locations are supported).")]
# Extract forecast URL from the response
properties = points_data.get("properties", {})
forecast_url = properties.get("forecast")
if not forecast_url:
return [types.TextContent(type="text", text="Failed to get forecast URL from grid point data")]
# Get the forecast
forecast_data = await make_nws_request(client, forecast_url)
if not forecast_data:
return [types.TextContent(type="text", text="Failed to retrieve forecast data")]
# Format the forecast periods
periods = forecast_data.get("properties", {}).get("periods", [])
if not periods:
return [types.TextContent(type="text", text="No forecast periods available")]
# Format each period into a concise string
formatted_forecast = []
for period in periods:
forecast_text = (
f"{period.get('name', 'Unknown')}:\n"
f"Temperature: {period.get('temperature', 'Unknown')}°{period.get('temperatureUnit', 'F')}\n"
f"Wind: {period.get('windSpeed', 'Unknown')} {period.get('windDirection', '')}\n"
f"{period.get('shortForecast', 'No forecast available')}\n"
"---"
)
formatted_forecast.append(forecast_text)
forecast_text = f"Forecast for {latitude}, {longitude}:\n\n" + "\n".join(formatted_forecast)
return [types.TextContent(
type="text",
text=forecast_text
)]
else:
raise ValueError(f"Unknown tool: {name}")
```
### Running the server
Finally, implement the main function to run the server:
```python
async def main():
# Run the server using stdin/stdout streams
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="weather",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
# This is needed if you'd like to connect to a custom client
if __name__ == "__main__":
asyncio.run(main())
```
Your server is complete! Run `uv run src/weather/server.py` to confirm that everything's working.
Let's now test your server from an existing MCP host, Claude for Desktop.
## Testing your server with Claude for Desktop
<Note>
Unfortunately, Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Building a client](/tutorials/building-a-client) tutorial for a workaround.
</Note>
First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download)
Next, open your Claude Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
For example, if you have [VS Code](https://code.visualstudio.com/) installed:
<Tabs>
<Tab title="MacOS/Linux">
```bash
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
```
</Tab>
<Tab title="Windows">
```powershell
code $env:AppData\Claude\claude_desktop_config.json
```
</Tab>
</Tabs>
Add this configuration (replace the parent folder path):
<Tabs>
<Tab title="MacOS/Linux">
```json Python
{
"mcpServers": {
"weather": {
"command": "uv",
"args": [
"--directory",
"/PATH/TO/PARENT/FOLDER/weather",
"run",
"weather"
]
}
}
}
```
</Tab>
<Tab title="Windows">
```json Python
{
"mcpServers": {
"weather": {
"command": "uv",
"args": [
"--directory",
"C:\\PATH\TO\PARENT\FOLDER\weather",
"run",
"weather"
]
}
}
}
```
</Tab>
</Tabs>
This tells Claude Desktop:
1. There's an MCP server named "weather"
2. Launch it by running `uv --directory /PATH/TO/PARENT/FOLDER/weather run weather`
Save the file, and restart **Claude Desktop**.
</Tab>
<Tab title="Node">
### Prerequisite knowledge
This quickstart assumes you have familiarity with:
- TypeScript
- LLMs like Claude
### System requirements
For TypeScript, make sure you have the latest version of Node installed.
### Set up your environment
First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
Verify your Node.js installation:
```bash
node --version
npm --version
```
For this tutorial, you'll need Node.js version 16 or higher.
Now, let's create and set up our project:
<CodeGroup>
```bash MacOS/Linux
@ -238,8 +561,6 @@ Create a `tsconfig.json` in the root of your project:
"exclude": ["node_modules"]
}
```
</Tab>
</Tabs>
Now let's dive into building your server.
@ -247,20 +568,6 @@ Now let's dive into building your server.
### Importing packages
<Tabs>
<Tab title="Python">
Add these to the top of your `server.py`:
```python
from typing import Any
import httpx
from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
import mcp.server.stdio
```
</Tab>
<Tab title="Node">
Add these to the top of your `src/index.ts`:
```typescript
@ -272,23 +579,9 @@ import {
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
```
</Tab>
</Tabs>
### Setting up the instance
<Tabs>
<Tab title="Python">
Then initialize the server instance and the base URL for the NWS API:
```python
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
server = Server("weather")
```
</Tab>
<Tab title="Node">
Then initialize the NWS API base URL, validation schemas, and server instance:
```typescript
@ -318,60 +611,9 @@ const server = new Server(
}
);
```
</Tab>
</Tabs>
### Implementing tool listing
<Tabs>
<Tab title="Python">
We need to tell clients what tools are available. The `list_tools()` decorator registers this handler:
```python
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
List available tools.
Each tool specifies its arguments using JSON Schema validation.
"""
return [
types.Tool(
name="get-alerts",
description="Get weather alerts for a state",
inputSchema={
"type": "object",
"properties": {
"state": {
"type": "string",
"description": "Two-letter state code (e.g. CA, NY)",
},
},
"required": ["state"],
},
),
types.Tool(
name="get-forecast",
description="Get weather forecast for a location",
inputSchema={
"type": "object",
"properties": {
"latitude": {
"type": "number",
"description": "Latitude of the location",
},
"longitude": {
"type": "number",
"description": "Longitude of the location",
},
},
"required": ["latitude", "longitude"],
},
),
]
```
</Tab>
<Tab title="Node">
We need to tell clients what tools are available. This `server.setRequestHandler` call will register this list for us:
```typescript
@ -415,8 +657,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
};
});
```
</Tab>
</Tabs>
This defines our two tools: `get-alerts` and `get-forecast`.
@ -424,37 +664,6 @@ This defines our two tools: `get-alerts` and `get-forecast`.
Next, let's add our helper functions for querying and formatting the data from the National Weather Service API:
<Tabs>
<Tab title="Python">
```python
async def make_nws_request(client: httpx.AsyncClient, url: str) -> dict[str, Any] | None:
"""Make a request to the NWS API with proper error handling."""
headers = {
"User-Agent": USER_AGENT,
"Accept": "application/geo+json"
}
try:
response = await client.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
def format_alert(feature: dict) -> str:
"""Format an alert feature into a concise string."""
props = feature["properties"]
return (
f"Event: {props.get('event', 'Unknown')}\n"
f"Area: {props.get('areaDesc', 'Unknown')}\n"
f"Severity: {props.get('severity', 'Unknown')}\n"
f"Status: {props.get('status', 'Unknown')}\n"
f"Headline: {props.get('headline', 'No headline')}\n"
"---"
)
```
</Tab>
<Tab title="Node">
```typescript
// Helper function for making NWS API requests
async function makeNWSRequest<T>(url: string): Promise<T | null> {
@ -523,127 +732,11 @@ interface ForecastResponse {
};
}
```
</Tab>
</Tabs>
### Implementing tool execution
The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
<Tabs>
<Tab title="Python">
```python
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
Handle tool execution requests.
Tools can fetch weather data and notify clients of changes.
"""
if not arguments:
raise ValueError("Missing arguments")
if name == "get-alerts":
state = arguments.get("state")
if not state:
raise ValueError("Missing state parameter")
# Convert state to uppercase to ensure consistent format
state = state.upper()
if len(state) != 2:
raise ValueError("State must be a two-letter code (e.g. CA, NY)")
async with httpx.AsyncClient() as client:
alerts_url = f"{NWS_API_BASE}/alerts?area={state}"
alerts_data = await make_nws_request(client, alerts_url)
if not alerts_data:
return [types.TextContent(type="text", text="Failed to retrieve alerts data")]
features = alerts_data.get("features", [])
if not features:
return [types.TextContent(type="text", text=f"No active alerts for {state}")]
# Format each alert into a concise string
formatted_alerts = [format_alert(feature) for feature in features[:20]] # only take the first 20 alerts
alerts_text = f"Active alerts for {state}:\n\n" + "\n".join(formatted_alerts)
return [
types.TextContent(
type="text",
text=alerts_text
)
]
elif name == "get-forecast":
try:
latitude = float(arguments.get("latitude"))
longitude = float(arguments.get("longitude"))
except (TypeError, ValueError):
return [types.TextContent(
type="text",
text="Invalid coordinates. Please provide valid numbers for latitude and longitude."
)]
# Basic coordinate validation
if not (-90 <= latitude <= 90) or not (-180 <= longitude <= 180):
return [types.TextContent(
type="text",
text="Invalid coordinates. Latitude must be between -90 and 90, longitude between -180 and 180."
)]
async with httpx.AsyncClient() as client:
# First get the grid point
lat_str = f"{latitude:.4f}"
lon_str = f"{longitude:.4f}"
points_url = f"{NWS_API_BASE}/points/{lat_str},{lon_str}"
points_data = await make_nws_request(client, points_url)
if not points_data:
return [types.TextContent(type="text", text=f"Failed to retrieve grid point data for coordinates: {latitude}, {longitude}. This location may not be supported by the NWS API (only US locations are supported).")]
# Extract forecast URL from the response
properties = points_data.get("properties", {})
forecast_url = properties.get("forecast")
if not forecast_url:
return [types.TextContent(type="text", text="Failed to get forecast URL from grid point data")]
# Get the forecast
forecast_data = await make_nws_request(client, forecast_url)
if not forecast_data:
return [types.TextContent(type="text", text="Failed to retrieve forecast data")]
# Format the forecast periods
periods = forecast_data.get("properties", {}).get("periods", [])
if not periods:
return [types.TextContent(type="text", text="No forecast periods available")]
# Format each period into a concise string
formatted_forecast = []
for period in periods:
forecast_text = (
f"{period.get('name', 'Unknown')}:\n"
f"Temperature: {period.get('temperature', 'Unknown')}°{period.get('temperatureUnit', 'F')}\n"
f"Wind: {period.get('windSpeed', 'Unknown')} {period.get('windDirection', '')}\n"
f"{period.get('shortForecast', 'No forecast available')}\n"
"---"
)
formatted_forecast.append(forecast_text)
forecast_text = f"Forecast for {latitude}, {longitude}:\n\n" + "\n".join(formatted_forecast)
return [types.TextContent(
type="text",
text=forecast_text
)]
else:
raise ValueError(f"Unknown tool: {name}")
```
</Tab>
<Tab title="Node">
```typescript
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
@ -792,34 +885,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
});
```
</Tab>
</Tabs>
### Running the server
Finally, implement the main function to run the server:
<Tabs>
<Tab title="Python">
```python
async def main():
# Run the server using stdin/stdout streams
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="weather",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
```
</Tab>
<Tab title="Node">
```typescript
// Start the server
async function main() {
@ -835,10 +905,6 @@ main().catch((error) => {
```
Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
</Tab>
</Tabs>
Your server is complete! Run `uv run src/weather/server.py` to confirm that everything's working.
Let's now test your server from an existing MCP host, Claude for Desktop.
@ -873,22 +939,6 @@ Add this configuration (replace the parent folder path):
<Tabs>
<Tab title="MacOS/Linux">
<CodeGroup>
```json Python
{
"mcpServers": {
"weather": {
"command": "uv",
"args": [
"--directory",
"/PATH/TO/PARENT/FOLDER/weather",
"run",
"weather"
]
}
}
}
```
```json Node
{
"mcpServers": {
@ -905,45 +955,29 @@ Add this configuration (replace the parent folder path):
</Tab>
<Tab title="Windows">
<CodeGroup>
```json Python
{
"mcpServers": {
"weather": {
"command": "uv",
"args": [
"--directory",
"C:\\PATH\TO\PARENT\FOLDER\weather",
"run",
"weather"
]
}
}
}
```
```json Node
{
"mcpServers": {
"weather": {
"command": "node",
"args": [
"C:\\PATH\TO\PARENT\FOLDER\weather\build\index.ts"
"C:\\PATH\TO\PARENT\FOLDER\weather\build\index.js"
]
}
}
}
```
</CodeGroup>
</Tab>
</Tabs>
This tells Claude Desktop:
1. There's an MCP server named "weather"
2. Launch it by running `uv --directory /PATH/TO/PARENT/FOLDER/weather run weather` if using Python or `node /PATH/TO/PARENT/FOLDER/weather/build/index.js` if using Node
2. Launch it by running `node /PATH/TO/PARENT/FOLDER/weather/build/index.js`
Save the file, and restart **Claude Desktop**.
</Tab>
</Tabs>
### Test with commands
@ -1039,7 +1073,7 @@ If Claude attempts to use the tools but they fail:
icon="outlet"
href="/tutorials/building-a-client"
>
Learn how to build your first MCP client
Learn how to build your an MCP client that can connect to your server
</Card>
<Card
title="Building a local fileserver"

View file

@ -5,35 +5,42 @@ description: "Learn how to build your first client in MCP"
In this tutorial, you'll learn how to build a client application that connects to MCP servers. It helps to have gone through the [Quickstart tutorial](/quickstart) that guides you through the basic of building your first server.
<Tabs>
<Tab title="Python">
## System Requirements
Before starting, ensure your system meets these requirements:
- Mac or Windows computer
- Latest Python version installed
- Latest version of `uv` installed
## Setting Up Your Environment
First, create and activate a virtual environment:
First, create a new Python project with `uv`:
```bash
# Install virtualenv if needed
pip install virtualenv
# Create project directory
mkdir mcp-client
uv init mcp-client
cd mcp-client
# Create virtual environment
python -m virtualenv venv
uv venv
# Activate virtual environment
# On Windows:
venv\Scripts\activate
.venv\Scripts\activate
# On Unix or MacOS:
source venv/bin/activate
source .venv/bin/activate
# Install required packages
pip install mcp anthropic python-dotenv
uv add mcp anthropic python-dotenv
# Remove boilerplate files
rm hello.py
# Create our main file
touch client.py
```
## Setting Up Your API Key
@ -57,9 +64,14 @@ Add `.env` to your `.gitignore`:
echo ".env" >> .gitignore
```
<Warning>
Make sure you keep your `ANTHROPIC_API_KEY` secure!
</Warning>
## Creating the Client
Let's create a Python client that can connect to any MCP server. We'll call it `client.py`:
### Basic Client Structure
First, let's set up our imports and create the basic client class:
```python
import asyncio
@ -80,110 +92,144 @@ class MCPClient:
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.anthropic = Anthropic()
# methods will go here
```
async def connect_to_server(self, server_script_path: str):
"""Connect to an MCP server"""
server_params = StdioServerParameters(
command="python",
args=[server_script_path],
env=None
)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
await self.session.initialize()
# List available tools
response = await self.session.list_tools()
tools = response.tools
print("\nConnected to server with tools:", [tool.name for tool in tools])
### Server Connection Management
async def process_query(self, query: str) -> str:
"""Process a query using Claude and available tools"""
messages = [
{
"role": "user",
"content": query
}
]
Next, we'll implement the method to connect to an MCP server:
response = await self.session.list_tools()
available_tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
} for tool in response.tools]
# Initial Claude API call
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
tools=available_tools
)
# Process response and handle tool calls
tool_results = []
final_text = []
for content in response.content:
if content.type == 'text':
final_text.append(content.text)
elif content.type == 'tool_use':
tool_name = content.name
tool_args = content.input
# Execute tool call
result = await self.session.call_tool(tool_name, tool_args)
tool_results.append({"call": tool_name, "result": result})
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
# Continue conversation with tool results
if hasattr(content, 'text') and content.text:
messages.append({
"role": "assistant",
"content": content.text
})
messages.append({
"role": "user",
"content": result.content
})
# Get next response from Claude
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
)
final_text.append(response.content[0].text)
return "\n".join(final_text)
async def chat_loop(self):
"""Run an interactive chat loop"""
print("\nMCP Client Started!")
print("Type your queries or 'quit' to exit.")
while True:
try:
query = input("\nQuery: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query)
print("\n" + response)
except Exception as e:
print(f"\nError: {str(e)}")
```python
async def connect_to_server(self, server_script_path: str):
"""Connect to an MCP server
async def cleanup(self):
"""Clean up resources"""
await self.exit_stack.aclose()
Args:
server_script_path: Path to the server script (.py or .js)
"""
is_python = server_script_path.endswith('.py')
is_js = server_script_path.endswith('.js')
if not (is_python or is_js):
raise ValueError("Server script must be a .py or .js file")
command = "python" if is_python else "node"
server_params = StdioServerParameters(
command=command,
args=[server_script_path],
env=None
)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
await self.session.initialize()
# List available tools
response = await self.session.list_tools()
tools = response.tools
print("\nConnected to server with tools:", [tool.name for tool in tools])
```
### Query Processing Logic
Now let's add the core functionality for processing queries and handling tool calls:
```python
async def process_query(self, query: str) -> str:
"""Process a query using Claude and available tools"""
messages = [
{
"role": "user",
"content": query
}
]
response = await self.session.list_tools()
available_tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
} for tool in response.tools]
# Initial Claude API call
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
tools=available_tools
)
# Process response and handle tool calls
tool_results = []
final_text = []
for content in response.content:
if content.type == 'text':
final_text.append(content.text)
elif content.type == 'tool_use':
tool_name = content.name
tool_args = content.input
# Execute tool call
result = await self.session.call_tool(tool_name, tool_args)
tool_results.append({"call": tool_name, "result": result})
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
# Continue conversation with tool results
if hasattr(content, 'text') and content.text:
messages.append({
"role": "assistant",
"content": content.text
})
messages.append({
"role": "user",
"content": result.content
})
# Get next response from Claude
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
)
final_text.append(response.content[0].text)
return "\n".join(final_text)
```
### Interactive Chat Interface
Now we'll add the chat loop and cleanup functionality:
```python
async def chat_loop(self):
"""Run an interactive chat loop"""
print("\nMCP Client Started!")
print("Type your queries or 'quit' to exit.")
while True:
try:
query = input("\nQuery: ").strip()
if query.lower() == 'quit':
break
response = await self.process_query(query)
print("\n" + response)
except Exception as e:
print(f"\nError: {str(e)}")
async def cleanup(self):
"""Clean up resources"""
await self.exit_stack.aclose()
```
### Main Entry Point
Finally, we'll add the main execution logic:
```python
async def main():
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
@ -201,31 +247,62 @@ if __name__ == "__main__":
asyncio.run(main())
```
## Understanding the Client Code
You can find the complete `client.py` file [here.](https://gist.github.com/zckly/f3f28ea731e096e53b39b47bf0a2d4b1)
Let's break down the key components of our MCP client:
## Key Components Explained
1. **MCPClient Class**
- Manages the connection to the MCP server
- Handles communication between Claude and the server's tools
- Maintains the chat session
### 1. Client Initialization
- The `MCPClient` class initializes with session management and API clients
- Uses `AsyncExitStack` for proper resource management
- Configures the Anthropic client for Claude interactions
2. **Key Methods**
- `connect_to_server()`: Establishes connection with an MCP server
- `process_query()`: Handles user queries by:
- Getting available tools from the server
- Sending the query to Claude with tool information
- Executing any tool calls Claude requests
- Processing Claude's responses
- `chat_loop()`: Runs the interactive chat interface
- `cleanup()`: Properly closes connections and resources
### 2. Server Connection
- Supports both Python and Node.js servers
- Validates server script type
- Sets up proper communication channels
- Initializes the session and lists available tools
### 3. Query Processing
- Maintains conversation context
- Handles Claude's responses and tool calls
- Manages the message flow between Claude and tools
- Combines results into a coherent response
### 4. Interactive Interface
- Provides a simple command-line interface
- Handles user input and displays responses
- Includes basic error handling
- Allows graceful exit
### 5. Resource Management
- Proper cleanup of resources
- Error handling for connection issues
- Graceful shutdown procedures
## Common Customization Points
1. **Tool Handling**
- Modify `process_query()` to handle specific tool types
- Add custom error handling for tool calls
- Implement tool-specific response formatting
2. **Response Processing**
- Customize how tool results are formatted
- Add response filtering or transformation
- Implement custom logging
3. **User Interface**
- Add a GUI or web interface
- Implement rich console output
- Add command history or auto-completion
## Running the Client
To run your client with any MCP server:
```bash
python client.py path/to/server.py
uv run client.py path/to/server.py # python server
uv run client.py path/to/build/index.js # node server
```
The client will:
@ -236,6 +313,12 @@ The client will:
- See tool executions
- Get responses from Claude
Here's an example of what it should look like if connected to the weather server from the quickstart:
<Frame>
<img src="/images/client-claude-cli-python.png" />
</Frame>
## How It Works
When you submit a query:
@ -248,7 +331,7 @@ When you submit a query:
6. Claude provides a natural language response
7. The response is displayed to you
## Best Practices
## Best practices
1. **Error Handling**
- Always wrap tool calls in try-catch blocks
@ -265,17 +348,532 @@ When you submit a query:
- Validate server responses
- Be cautious with tool permissions
## Next Steps
## Troubleshooting
Now that you have a basic MCP client, you can:
1. Add support for different server types
2. Enhance error handling and recovery
3. Implement more sophisticated conversation handling
4. Add authentication and security features
5. Create a better user interface
### Server Path Issues
- Double-check the path to your server script is correct
- Use the absolute path if the relative path isn't working
- For Windows users, make sure to use forward slashes (/) or escaped backslashes (\\) in the path
- Verify the server file has the correct extension (.py for Python or .js for Node.js)
## Resources
Example of correct path usage:
```bash
# Relative path
uv run client.py ./server/weather.py
- [MCP Documentation](https://modelcontextprotocol.github.io/)
- [Claude API Documentation](https://docs.anthropic.com/claude/reference)
- [Tool Use Guide](https://docs.anthropic.com/claude/docs/function-calling)
# Absolute path
uv run client.py /Users/username/projects/mcp-server/weather.py
# Windows path (either format works)
uv run client.py C:/projects/mcp-server/weather.py
uv run client.py C:\\projects\\mcp-server\\weather.py
```
### Response Timing
- The first response might take up to 30 seconds to return
- This is normal and happens while:
- The server initializes
- Claude processes the query
- Tools are being executed
- Subsequent responses are typically faster
- Don't interrupt the process during this initial waiting period
### Common Error Messages
If you see:
- `FileNotFoundError`: Check your server path
- `Connection refused`: Ensure the server is running and the path is correct
- `Tool execution failed`: Verify the tool's required environment variables are set
- `Timeout error`: Consider increasing the timeout in your client configuration
</Tab>
<Tab title="Node">
## System Requirements
Before starting, ensure your system meets these requirements:
- Mac or Windows computer
- Node.js version 16 or higher installed
- npm (comes with Node.js)
## Setting Up Your Environment
First, create a new Node.js project:
```bash
# Create project directory
mkdir mcp-client
cd mcp-client
# Initialize npm project
npm init -y
# Install dependencies
npm install @modelcontextprotocol/sdk @anthropic-ai/sdk dotenv
npm install -D typescript @types/node
# Create TypeScript config
npx tsc --init
# Create necessary files
mkdir src
touch src/client.ts
touch .env
```
Update your `package.json` to add necessary configuration:
```json
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node build/client.js"
}
}
```
Update your `tsconfig.json` with appropriate settings:
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
```
## Setting Up Your API Key
You'll need an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys).
Create a `.env` file:
```bash
ANTHROPIC_API_KEY=your_key_here
```
Add `.env` to your `.gitignore`:
```bash
echo ".env" >> .gitignore
```
## Creating the Client
First, let's set up our imports and create the basic client class in `src/client.ts`:
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import Anthropic from "@anthropic-ai/sdk";
import dotenv from "dotenv";
import { Tool } from "@anthropic-ai/sdk/resources/messages.js";
import {
CallToolResultSchema,
ListToolsResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import * as readline from "node:readline";
dotenv.config();
interface MCPClientConfig {
name?: string;
version?: string;
}
class MCPClient {
private client: Client | null = null;
private anthropic: Anthropic;
private transport: StdioClientTransport | null = null;
constructor(config: MCPClientConfig = {}) {
this.anthropic = new Anthropic();
}
// Methods will go here
}
```
## Server Connection Management
Next, implement the method to connect to an MCP server:
```typescript
async connectToServer(serverScriptPath: string): Promise<void> {
const isPython = serverScriptPath.endsWith(".py");
const isJs = serverScriptPath.endsWith(".js");
if (!isPython && !isJs) {
throw new Error("Server script must be a .py or .js file");
}
const command = isPython ? "python" : "node";
this.transport = new StdioClientTransport({
command,
args: [serverScriptPath],
});
this.client = new Client(
{
name: "mcp-client",
version: "1.0.0",
},
{
capabilities: {},
}
);
await this.client.connect(this.transport);
// List available tools
const response = await this.client.request(
{ method: "tools/list" },
ListToolsResultSchema
);
console.log(
"\nConnected to server with tools:",
response.tools.map((tool: any) => tool.name)
);
}
```
## Query Processing Logic
Now add the core functionality for processing queries and handling tool calls:
```typescript
async processQuery(query: string): Promise<string> {
if (!this.client) {
throw new Error("Client not connected");
}
// Initialize messages array with user query
let messages: Anthropic.MessageParam[] = [
{
role: "user",
content: query,
},
];
// Get available tools
const toolsResponse = await this.client.request(
{ method: "tools/list" },
ListToolsResultSchema
);
const availableTools: Tool[] = toolsResponse.tools.map((tool: any) => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
}));
const finalText: string[] = [];
let currentResponse = await this.anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1000,
messages,
tools: availableTools,
});
// Process the response and any tool calls
while (true) {
// Add Claude's response to final text and messages
for (const content of currentResponse.content) {
if (content.type === "text") {
finalText.push(content.text);
} else if (content.type === "tool_use") {
const toolName = content.name;
const toolArgs = content.input;
// Execute tool call
const result = await this.client.request(
{
method: "tools/call",
params: {
name: toolName,
args: toolArgs,
},
},
CallToolResultSchema
);
finalText.push(
`[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`
);
// Add Claude's response (including tool use) to messages
messages.push({
role: "assistant",
content: currentResponse.content,
});
// Add tool result to messages
messages.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: content.id,
content: [
{ type: "text", text: JSON.stringify(result.content) },
],
},
],
});
// Get next response from Claude with tool results
currentResponse = await this.anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1000,
messages,
tools: availableTools,
});
// Add Claude's interpretation of the tool results to final text
if (currentResponse.content[0]?.type === "text") {
finalText.push(currentResponse.content[0].text);
}
// Continue the loop to process any additional tool calls
continue;
}
}
// If we reach here, there were no tool calls in the response
break;
}
return finalText.join("\n");
}
```
## Interactive Chat Interface
Add the chat loop and cleanup functionality:
```typescript
async chatLoop(): Promise<void> {
console.log("\nMCP Client Started!");
console.log("Type your queries or 'quit' to exit.");
// Using Node's readline for console input
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const askQuestion = () => {
rl.question("\nQuery: ", async (query: string) => {
try {
if (query.toLowerCase() === "quit") {
await this.cleanup();
rl.close();
return;
}
const response = await this.processQuery(query);
console.log("\n" + response);
askQuestion();
} catch (error) {
console.error("\nError:", error);
askQuestion();
}
});
};
askQuestion();
}
async cleanup(): Promise<void> {
if (this.transport) {
await this.transport.close();
}
}
```
## Main Entry Point
Finally, add the main execution logic outside the class:
```typescript
// Main execution
async function main() {
if (process.argv.length < 3) {
console.log("Usage: ts-node client.ts <path_to_server_script>");
process.exit(1);
}
const client = new MCPClient();
try {
await client.connectToServer(process.argv[2]);
await client.chatLoop();
} catch (error) {
console.error("Error:", error);
await client.cleanup();
process.exit(1);
}
}
// Run main if this is the main module
if (import.meta.url === new URL(process.argv[1], "file:").href) {
main();
}
export default MCPClient;
```
## Running the Client
To run your client with any MCP server:
```bash
# Build the TypeScript code. Make sure to rerun this every time you update `client.ts`!
npm run build
# Run the client
node build/client.js path/to/server.py # for Python servers
node build/client.js path/to/server.js # for Node.js servers
```
The client will:
1. Connect to the specified server
2. List available tools
3. Start an interactive chat session where you can:
- Enter queries
- See tool executions
- Get responses from Claude
## Key Components Explained
#### 1. Client Initialization
- The `MCPClient` class initializes with session management and API clients
- Sets up the MCP client with basic capabilities
- Configures the Anthropic client for Claude interactions
#### 2. Server Connection
- Supports both Python and Node.js servers
- Validates server script type
- Sets up proper communication channels
- Lists available tools on connection
#### 3. Query Processing
- Maintains conversation context
- Handles Claude's responses and tool calls
- Manages the message flow between Claude and tools
- Combines results into a coherent response
#### 4. Interactive Interface
- Provides a simple command-line interface
- Handles user input and displays responses
- Includes basic error handling
- Allows graceful exit
#### 5. Resource Management
- Proper cleanup of resources
- Error handling for connection issues
- Graceful shutdown procedures
### Common Customization Points
1. **Tool Handling**
- Modify `processQuery()` to handle specific tool types
- Add custom error handling for tool calls
- Implement tool-specific response formatting
2. **Response Processing**
- Customize how tool results are formatted
- Add response filtering or transformation
- Implement custom logging
3. **User Interface**
- Add a GUI or web interface
- Implement rich console output
- Add command history or auto-completion
### Best Practices
1. **Error Handling**
- Always wrap tool calls in try-catch blocks
- Provide meaningful error messages
- Gracefully handle connection issues
2. **Resource Management**
- Use proper cleanup methods
- Close connections when done
- Handle server disconnections
3. **Security**
- Store API keys securely in `.env`
- Validate server responses
- Be cautious with tool permissions
### Troubleshooting
#### Server Path Issues
- Double-check the path to your server script
- Use absolute paths if relative paths aren't working
- For Windows users, use forward slashes (/) or escaped backslashes (\\)
- Verify the server file has the correct extension (.py or .js)
Example of correct path usage:
```bash
# Relative path
node build/client.js ./server/weather.js
# Absolute path
node build/client.js /Users/username/projects/mcp-server/weather.js
# Windows path (either format works)
node build/client.js C:/projects/mcp-server/weather.js
node build/client.js C:\\projects\\mcp-server\\weather.js
```
#### Connection Issues
- Verify the server script exists and has correct permissions
- Check that the server script is executable
- Ensure the server script's dependencies are installed
- Try running the server script directly to check for errors
#### Tool Execution Issues
- Check server logs for error messages
- Verify tool input arguments match the schema
- Ensure tool dependencies are available
- Add debug logging to track execution flow
</Tab>
</Tabs>
## Next steps
<CardGroup cols={1}>
<Card
title="Building a local fileserver"
icon="files"
href="/examples"
>
Build an intermediate MCP server that communicates with your local filesystem
</Card>
<Card
title="Example servers"
icon="grid"
href="/examples"
>
Check out our gallery of official MCP servers and implementations
</Card>
<Card
title="Core architecture"
icon="sitemap"
href="/docs/concepts/architecture"
>
Understand how MCP connects clients, servers, and LLMs
</Card>
</CardGroup>