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

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"