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