diff --git a/images/client-claude-cli-python.png b/images/client-claude-cli-python.png
new file mode 100644
index 0000000..0997ebc
Binary files /dev/null and b/images/client-claude-cli-python.png differ
diff --git a/quickstart.mdx b/quickstart.mdx
index b64a35b..a9a9335 100644
--- a/quickstart.mdx
+++ b/quickstart.mdx
@@ -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.
+
+
+
### 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
-
-
First, let's install `uv` and set up our Python project and environment:
@@ -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.
-
-
-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:
-
-
```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']
```
-
+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
+
+
+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.
+
+
+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:
+
+
+
+```bash
+code ~/Library/Application\ Support/Claude/claude_desktop_config.json
+```
+
+
+```powershell
+code $env:AppData\Claude\claude_desktop_config.json
+```
+
+
+
+
+Add this configuration (replace the parent folder path):
+
+
+
+```json Python
+{
+ "mcpServers": {
+ "weather": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "/PATH/TO/PARENT/FOLDER/weather",
+ "run",
+ "weather"
+ ]
+ }
+ }
+}
+```
+
+
+```json Python
+{
+ "mcpServers": {
+ "weather": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "C:\\PATH\TO\PARENT\FOLDER\weather",
+ "run",
+ "weather"
+ ]
+ }
+ }
+}
+```
+
+
+
+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**.
+
+### 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:
```bash MacOS/Linux
@@ -238,8 +561,6 @@ Create a `tsconfig.json` in the root of your project:
"exclude": ["node_modules"]
}
```
-
-
Now let's dive into building your server.
@@ -247,20 +568,6 @@ Now let's dive into building your server.
### Importing packages
-
-
-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
-```
-
-
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";
```
-
-
### 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")
-```
-
-
Then initialize the NWS API base URL, validation schemas, and server instance:
```typescript
@@ -318,60 +611,9 @@ const server = new Server(
}
);
```
-
-
### 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"],
- },
- ),
- ]
-
-```
-
-
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 () => {
};
});
```
-
-
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:
-
-
-```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"
- "---"
- )
-```
-
-
```typescript
// Helper function for making NWS API requests
async function makeNWSRequest(url: string): Promise {
@@ -523,127 +732,11 @@ interface ForecastResponse {
};
}
```
-
-
### 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:.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}")
-```
-
-
-
```typescript
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -792,34 +885,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
});
```
-
-
### 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={},
- ),
- ),
- )
-```
-
-
```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.
-
-
-
-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):
-```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):
-
-```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"
]
}
}
}
```
-
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**.
+
+
### 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
+
+
## 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
```
+
+Make sure you keep your `ANTHROPIC_API_KEY` secure!
+
+
## 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 ")
@@ -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:
+
+
+
+
+
## 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)
\ No newline at end of file
+# 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
+
+
+
+## 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 ");
+ 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
+
+
+
+## Next steps
+
+
+
+ Build an intermediate MCP server that communicates with your local filesystem
+
+
+ Check out our gallery of official MCP servers and implementations
+
+
+ Understand how MCP connects clients, servers, and LLMs
+
+
\ No newline at end of file