tested on mac for both python and node
This commit is contained in:
parent
920c05d114
commit
de7df3fd78
7 changed files with 703 additions and 85 deletions
BIN
images/available-mcp-tools.png
Normal file
BIN
images/available-mcp-tools.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
BIN
images/current-weather.png
Normal file
BIN
images/current-weather.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 756 KiB |
BIN
images/visual-indicator-mcp-tools.png
Normal file
BIN
images/visual-indicator-mcp-tools.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
images/weather-alerts.png
Normal file
BIN
images/weather-alerts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
792
quickstart.mdx
792
quickstart.mdx
|
|
@ -1,11 +1,9 @@
|
||||||
---
|
---
|
||||||
title: "Quickstart"
|
title: "Quickstart"
|
||||||
description: "Get started with building your first MCP server and connecting it to an MCP host"
|
description: "Get started with building your first MCP server and connecting it to a host"
|
||||||
---
|
---
|
||||||
|
|
||||||
In this tutorial, we'll build a simple MCP server with a `get_weather` tool and connect it to a popular MCP host, Claude for Desktop. We'll start with a basic setup, and then progress to more complex use cases.
|
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.
|
||||||
|
|
||||||
You can think of MCP as a bridge between your LLMs and your data and tools. Model Context Protocol was developed by Anthropic (the makers of [Claude](https://claude.ai)), but MCP can work with any Large Language Model provider that supports tool calling.
|
|
||||||
|
|
||||||
### What is MCP?
|
### What is MCP?
|
||||||
|
|
||||||
|
|
@ -18,13 +16,20 @@ MCP is an open protocol that standardizes how applications provide context to LL
|
||||||
|
|
||||||
### What we'll be building
|
### What we'll be building
|
||||||
|
|
||||||
Many LLMs (including Claude) do not currently have the ability to fetch the current temperature and weather conditions. Let's use MCP to solve that!
|
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!
|
||||||
|
|
||||||
|
We'll build a server that exposes two tools: `get-alerts` and `get-forecast`. Then we'll connect the server to an MCP host (in this case, Claude Desktop):
|
||||||
|
|
||||||
<Frame>
|
<Frame>
|
||||||
<img src="/images/quickstart-weather.png" />
|
<img src="/images/weather-alerts.png" />
|
||||||
|
</Frame>
|
||||||
|
<Frame>
|
||||||
|
<img src="/images/current-weather.png" />
|
||||||
</Frame>
|
</Frame>
|
||||||
|
|
||||||
Servers can connect to any host. We'll walk through using Claude Desktop as the host and eventually show you how to build your own.
|
<Note>
|
||||||
|
Servers can connect to any host. We've chosen Claude desktop here for simplicity, but we also have guides on [building your own client](/tutorials/building-a-client)
|
||||||
|
</Note>
|
||||||
|
|
||||||
<Accordion title="Why Claude Desktop and not Claude.ai?">
|
<Accordion title="Why Claude Desktop and not Claude.ai?">
|
||||||
Because servers are locally run, MCP currently only supports desktop hosts. Remote hosts are in active development.
|
Because servers are locally run, MCP currently only supports desktop hosts. Remote hosts are in active development.
|
||||||
|
|
@ -50,7 +55,7 @@ It also helps to be familiar with [tool use (aka function calling)](https://docs
|
||||||
|
|
||||||
### System requirements
|
### System requirements
|
||||||
|
|
||||||
For Python, make sure you have the latest version of Python 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. For TypeScript, make sure you have the latest version of Node installed.
|
||||||
|
|
||||||
### Set up your environment
|
### Set up your environment
|
||||||
|
|
||||||
|
|
@ -64,11 +69,14 @@ First, let's install `uv` and set up our Python project and environment:
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash Windows
|
```powershell Windows
|
||||||
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||||
```
|
```
|
||||||
|
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
|
Make sure to restart your terminal afterwards to ensure that the `uv` command gets picked up.
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Node">
|
<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/).
|
First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).
|
||||||
|
|
@ -98,17 +106,16 @@ source .venv/bin/activate
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
uv pip install mcp httpx
|
uv pip install mcp httpx
|
||||||
|
|
||||||
# Clean up boilerplate code
|
# Remove template file
|
||||||
rm -rf hello.py
|
rm hello.py
|
||||||
|
|
||||||
# Create our files
|
# Create our files
|
||||||
mkdir src
|
mkdir -p src/weather
|
||||||
mkdir src/weather
|
|
||||||
touch src/weather/__init__.py
|
touch src/weather/__init__.py
|
||||||
touch src/weather/server.py
|
touch src/weather/server.py
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash Windows
|
```powershell Windows
|
||||||
# Create a new directory for our project
|
# Create a new directory for our project
|
||||||
uv init mcp-quickstart
|
uv init mcp-quickstart
|
||||||
cd mcp-quickstart
|
cd mcp-quickstart
|
||||||
|
|
@ -121,13 +128,13 @@ uv venv
|
||||||
uv pip install mcp httpx
|
uv pip install mcp httpx
|
||||||
|
|
||||||
# Clean up boilerplate code
|
# Clean up boilerplate code
|
||||||
del /f /q hello.py
|
rm hello.py
|
||||||
|
|
||||||
# Create our files
|
# Create our files
|
||||||
md src
|
md src
|
||||||
md src\weather
|
md src\weather
|
||||||
echo.> src\__init__.py
|
new-item src\weather\__init__.py
|
||||||
echo.> src\server.py
|
new-item src\weather\server.py
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
|
|
@ -160,7 +167,9 @@ __all__ = ['main', 'server']
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="Node">
|
<Tab title="Node">
|
||||||
```bash
|
|
||||||
|
<CodeGroup>
|
||||||
|
```bash MacOS/Linux
|
||||||
# Create a new directory for our project
|
# Create a new directory for our project
|
||||||
mkdir mcp-quickstart
|
mkdir mcp-quickstart
|
||||||
cd mcp-quickstart
|
cd mcp-quickstart
|
||||||
|
|
@ -169,23 +178,39 @@ cd mcp-quickstart
|
||||||
npm init -y
|
npm init -y
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
npm install @modelcontextprotocol/sdk node-fetch
|
npm install @modelcontextprotocol/sdk zod
|
||||||
npm install -D @types/node typescript
|
npm install -D @types/node typescript
|
||||||
```
|
|
||||||
|
|
||||||
Then create a new file called `src/index.ts` in your project directory:
|
# Create our files
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir src
|
mkdir src
|
||||||
touch src/index.ts
|
touch src/index.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```powershell Windows
|
||||||
|
# Create a new directory for our project
|
||||||
|
md mcp-quickstart
|
||||||
|
cd mcp-quickstart
|
||||||
|
|
||||||
|
# Initialize a new npm project
|
||||||
|
npm init -y
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install @modelcontextprotocol/sdk zod
|
||||||
|
npm install -D @types/node typescript
|
||||||
|
|
||||||
|
# Create our files
|
||||||
|
md src
|
||||||
|
new-item src\index.ts
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
Update your package.json to add type: "module" and a build script:
|
Update your package.json to add type: "module" and a build script:
|
||||||
|
|
||||||
```json package.json
|
```json package.json
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"mcp-quickstart": "./build/index.js"
|
"weather": "./build/index.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
||||||
|
|
@ -196,7 +221,7 @@ Update your package.json to add type: "module" and a build script:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a tsconfig.json:
|
Create a `tsconfig.json` in the root of your project:
|
||||||
|
|
||||||
```json tsconfig.json
|
```json tsconfig.json
|
||||||
{
|
{
|
||||||
|
|
@ -224,113 +249,563 @@ Now let's dive into building your server.
|
||||||
|
|
||||||
### Importing packages
|
### Importing packages
|
||||||
|
|
||||||
Add these to your server.py:
|
<Tabs>
|
||||||
|
<Tab title="Python">
|
||||||
|
Add these to the top of your `server.py`:
|
||||||
```python
|
```python
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from mcp.server.models import InitializationOptions
|
from mcp.server.models import InitializationOptions
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
from mcp.server import NotificationOptions, Server
|
from mcp.server import NotificationOptions, Server
|
||||||
import mcp.server.stdio
|
import mcp.server.stdio
|
||||||
```
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Node">
|
||||||
|
Add these to the top of your `src/index.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
### Setting up the instance
|
### Setting up the instance
|
||||||
|
|
||||||
Then add the cache, server instance and the base URL for the NWS API:
|
<Tabs>
|
||||||
|
<Tab title="Python">
|
||||||
|
Then initialize the server instance and the base URL for the NWS API:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Cache weather data
|
|
||||||
weather_cache: dict[str, Any] = {}
|
|
||||||
NWS_API_BASE = "https://api.weather.gov"
|
NWS_API_BASE = "https://api.weather.gov"
|
||||||
|
USER_AGENT = "weather-app/1.0"
|
||||||
|
|
||||||
server = Server("weather")
|
server = Server("weather")
|
||||||
```
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Node">
|
||||||
|
Then initialize the NWS API base URL, validation schemas, and server instance:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const NWS_API_BASE = "https://api.weather.gov";
|
||||||
|
const USER_AGENT = "weather-app/1.0";
|
||||||
|
|
||||||
|
// Define Zod schemas for validation
|
||||||
|
const AlertsArgumentsSchema = z.object({
|
||||||
|
state: z.string().length(2),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ForecastArgumentsSchema = z.object({
|
||||||
|
latitude: z.number().min(-90).max(90),
|
||||||
|
longitude: z.number().min(-180).max(180),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create server instance
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: "weather",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
### Implementing tool listing
|
### Implementing tool listing
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Python">
|
||||||
We need to tell clients what tools are available. The `list_tools()` decorator registers this handler:
|
We need to tell clients what tools are available. The `list_tools()` decorator registers this handler:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@server.list_tools()
|
@server.list_tools()
|
||||||
async def handle_list_tools() -> list[types.Tool]:
|
async def handle_list_tools() -> list[types.Tool]:
|
||||||
|
"""
|
||||||
|
List available tools.
|
||||||
|
Each tool specifies its arguments using JSON Schema validation.
|
||||||
|
"""
|
||||||
return [
|
return [
|
||||||
types.Tool(
|
types.Tool(
|
||||||
name="get-weather",
|
name="get-alerts",
|
||||||
description="Get weather data for a location",
|
description="Get weather alerts for a state",
|
||||||
inputSchema={
|
inputSchema={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"latitude": {"type": "number"},
|
"state": {
|
||||||
"longitude": {"type": "number"},
|
"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"],
|
"required": ["latitude", "longitude"],
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
```
|
|
||||||
|
|
||||||
This defines a single tool that requires latitude and longitude parameters.
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Node">
|
||||||
|
We need to tell clients what tools are available. This `server.setRequestHandler` call will register this list for us:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// List available tools
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
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"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
<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> {
|
||||||
|
const headers = {
|
||||||
|
"User-Agent": USER_AGENT,
|
||||||
|
Accept: "application/geo+json",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return (await response.json()) as T;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error making NWS request:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AlertFeature {
|
||||||
|
properties: {
|
||||||
|
event?: string;
|
||||||
|
areaDesc?: string;
|
||||||
|
severity?: string;
|
||||||
|
status?: string;
|
||||||
|
headline?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format alert data
|
||||||
|
function formatAlert(feature: AlertFeature): string {
|
||||||
|
const props = feature.properties;
|
||||||
|
return [
|
||||||
|
`Event: ${props.event || "Unknown"}`,
|
||||||
|
`Area: ${props.areaDesc || "Unknown"}`,
|
||||||
|
`Severity: ${props.severity || "Unknown"}`,
|
||||||
|
`Status: ${props.status || "Unknown"}`,
|
||||||
|
`Headline: ${props.headline || "No headline"}`,
|
||||||
|
"---",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForecastPeriod {
|
||||||
|
name?: string;
|
||||||
|
temperature?: number;
|
||||||
|
temperatureUnit?: string;
|
||||||
|
windSpeed?: string;
|
||||||
|
windDirection?: string;
|
||||||
|
shortForecast?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AlertsResponse {
|
||||||
|
features: AlertFeature[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PointsResponse {
|
||||||
|
properties: {
|
||||||
|
forecast?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForecastResponse {
|
||||||
|
properties: {
|
||||||
|
periods: ForecastPeriod[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
### Implementing tool execution
|
### Implementing tool execution
|
||||||
|
|
||||||
The tool execution handler processes requests to get weather data:
|
The tool execution handler is responsible for actually executing the logic of each tool. Let's add it:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Python">
|
||||||
```python
|
```python
|
||||||
@server.call_tool()
|
@server.call_tool()
|
||||||
async def handle_call_tool(
|
async def handle_call_tool(
|
||||||
name: str,
|
name: str, arguments: dict | None
|
||||||
arguments: dict | None
|
|
||||||
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
||||||
# Validate tool name and arguments
|
"""
|
||||||
if name != "get-weather":
|
Handle tool execution requests.
|
||||||
raise ValueError(f"Unknown tool: {name}")
|
Tools can fetch weather data and notify clients of changes.
|
||||||
|
"""
|
||||||
if not arguments:
|
if not arguments:
|
||||||
raise ValueError("Missing arguments")
|
raise ValueError("Missing arguments")
|
||||||
|
|
||||||
lat = arguments.get("latitude")
|
if name == "get-alerts":
|
||||||
lon = arguments.get("longitude")
|
state = arguments.get("state")
|
||||||
if lat is None or lon is None:
|
if not state:
|
||||||
raise ValueError("Missing latitude or longitude")
|
raise ValueError("Missing state parameter")
|
||||||
|
|
||||||
# Fetch weather data
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
# Get grid point
|
|
||||||
points_url = f"{NWS_API_BASE}/points/{lat},{lon}"
|
|
||||||
points_response = await client.get(
|
|
||||||
points_url,
|
|
||||||
headers={"Accept": "application/json"}
|
|
||||||
)
|
|
||||||
points_data = points_response.json()
|
|
||||||
|
|
||||||
# Get forecast
|
|
||||||
forecast_url = points_data["properties"]["forecast"]
|
|
||||||
forecast_response = await client.get(
|
|
||||||
forecast_url,
|
|
||||||
headers={"Accept": "application/json"}
|
|
||||||
)
|
|
||||||
forecast_data = forecast_response.json()
|
|
||||||
|
|
||||||
# Cache the forecast data
|
# Convert state to uppercase to ensure consistent format
|
||||||
location_key = f"{lat},{lon}"
|
state = state.upper()
|
||||||
weather_cache[location_key] = forecast_data
|
if len(state) != 2:
|
||||||
|
raise ValueError("State must be a two-letter code (e.g. CA, NY)")
|
||||||
# Notify clients of resource changes
|
|
||||||
await server.request_context.session.send_resource_list_changed()
|
async with httpx.AsyncClient() as client:
|
||||||
|
alerts_url = f"{NWS_API_BASE}/alerts?area={state}"
|
||||||
return [
|
alerts_data = await make_nws_request(client, alerts_url)
|
||||||
types.TextContent(
|
|
||||||
|
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]
|
||||||
|
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",
|
type="text",
|
||||||
text=f"Retrieved weather data for location ({lat}, {lon})",
|
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) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (name === "get-alerts") {
|
||||||
|
const { state } = AlertsArgumentsSchema.parse(args);
|
||||||
|
const stateCode = state.toUpperCase();
|
||||||
|
|
||||||
|
const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
|
||||||
|
const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
|
||||||
|
|
||||||
|
if (!alertsData) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Failed to retrieve alerts data",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const features = alertsData.features || [];
|
||||||
|
if (features.length === 0) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `No active alerts for ${stateCode}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedAlerts = features.map(formatAlert);
|
||||||
|
const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join(
|
||||||
|
"\n"
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: alertsText,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else if (name === "get-forecast") {
|
||||||
|
const { latitude, longitude } = ForecastArgumentsSchema.parse(args);
|
||||||
|
|
||||||
|
// Get grid point data
|
||||||
|
const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(
|
||||||
|
4
|
||||||
|
)},${longitude.toFixed(4)}`;
|
||||||
|
const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
|
||||||
|
|
||||||
|
if (!pointsData) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `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).`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const forecastUrl = pointsData.properties?.forecast;
|
||||||
|
if (!forecastUrl) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Failed to get forecast URL from grid point data",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get forecast data
|
||||||
|
const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
|
||||||
|
if (!forecastData) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Failed to retrieve forecast data",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const periods = forecastData.properties?.periods || [];
|
||||||
|
if (periods.length === 0) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "No forecast periods available",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format forecast periods
|
||||||
|
const formattedForecast = periods.map((period: ForecastPeriod) =>
|
||||||
|
[
|
||||||
|
`${period.name || "Unknown"}:`,
|
||||||
|
`Temperature: ${period.temperature || "Unknown"}°${
|
||||||
|
period.temperatureUnit || "F"
|
||||||
|
}`,
|
||||||
|
`Wind: ${period.windSpeed || "Unknown"} ${
|
||||||
|
period.windDirection || ""
|
||||||
|
}`,
|
||||||
|
`${period.shortForecast || "No forecast available"}`,
|
||||||
|
"---",
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join(
|
||||||
|
"\n"
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: forecastText,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown tool: ${name}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid arguments: ${error.errors
|
||||||
|
.map((e) => `${e.path.join(".")}: ${e.message}`)
|
||||||
|
.join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
### Running the server
|
### Running the server
|
||||||
|
|
||||||
Finally, implement the main function to run the server:
|
Finally, implement the main function to run the server:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Python">
|
||||||
```python
|
```python
|
||||||
async def main():
|
async def main():
|
||||||
|
# Run the server using stdin/stdout streams
|
||||||
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
||||||
await server.run(
|
await server.run(
|
||||||
read_stream,
|
read_stream,
|
||||||
|
|
@ -345,6 +820,25 @@ async def main():
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Node">
|
||||||
|
```typescript
|
||||||
|
// Start the server
|
||||||
|
async function main() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
console.error("Weather MCP Server running on stdio");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("Fatal error in main():", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
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! Let's now test your server from an existing MCP host, Claude for Desktop.
|
Your server is complete! Let's now test your server from an existing MCP host, Claude for Desktop.
|
||||||
|
|
||||||
|
|
@ -356,7 +850,7 @@ Unfortunately, Claude for Desktop is not yet available on Linux. Linux users can
|
||||||
|
|
||||||
First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download)
|
First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download)
|
||||||
|
|
||||||
Open your Claude Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor.
|
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:
|
For example, if you have [VS Code](https://code.visualstudio.com/) installed:
|
||||||
|
|
||||||
|
|
@ -378,7 +872,8 @@ Add this configuration (replace the parent folder path):
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="MacOS/Linux">
|
<Tab title="MacOS/Linux">
|
||||||
```json
|
<CodeGroup>
|
||||||
|
```json Python
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"weather": {
|
"weather": {
|
||||||
|
|
@ -393,9 +888,25 @@ Add this configuration (replace the parent folder path):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```json Node
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"weather": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"/PATH/TO/PARENT/FOLDER/mcp-quickstart/build/index.js",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Windows">
|
<Tab title="Windows">
|
||||||
```json
|
<CodeGroup>
|
||||||
|
|
||||||
|
```json Python
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"weather": {
|
"weather": {
|
||||||
|
|
@ -410,24 +921,56 @@ Add this configuration (replace the parent folder path):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```json Node
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"weather": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"C:\\PATH\TO\PARENT\FOLDER\mcp-quickstart\build\index.ts",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeGroup>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
This tells Claude Desktop:
|
This tells Claude Desktop:
|
||||||
1. There's an MCP server named "weather"
|
1. There's an MCP server named "weather"
|
||||||
2. Launch it by running `uv --directory /PATH/TO/PARENT/FOLDER/mcp-quickstart run weather`
|
2. Launch it by running `uv --directory /PATH/TO/PARENT/FOLDER/mcp-quickstart run weather` if using Python or `node /PATH/TO/PARENT/FOLDER/mcp-quickstart/build/index.js` if using Node
|
||||||
|
|
||||||
Save the file, and restart **Claude Desktop**.
|
Save the file, and restart **Claude Desktop**.
|
||||||
|
|
||||||
### Test with commands
|
### Test with commands
|
||||||
|
|
||||||
You can now test your server by running the following commands in Claude Desktop:
|
First, make sure Claude Desktop is picking up the two tools we've exposed in our `weather` server. You can do this by looking for the hammer <img src="/images/claude-desktop-mcp-hammer-icon.svg" style={{display: 'inline', margin: 0, height: '1.3em'}} /> icon:
|
||||||
|
|
||||||
- What's the weather in Brooklyn?
|
|
||||||
- What's the weather in San Francisco?
|
|
||||||
|
|
||||||
<Frame>
|
<Frame>
|
||||||
<img src="/images/quickstart-weather.png" />
|
<img src="/images/visual-indicator-mcp-tools.png" />
|
||||||
|
</Frame>
|
||||||
|
|
||||||
|
After clicking on the hammer icon, you should see two tools listed:
|
||||||
|
|
||||||
|
<Frame>
|
||||||
|
<img src="/images/available-mcp-tools.png" />
|
||||||
|
</Frame>
|
||||||
|
|
||||||
|
If your server isn't being picked up by Claude Desktop, proceed to the **Troubleshooting** section for debugging advice.
|
||||||
|
|
||||||
|
You can now test your server by running the following commands in Claude Desktop:
|
||||||
|
|
||||||
|
- What's the weather in Sacramento?
|
||||||
|
- What are the active weather alerts in Texas?
|
||||||
|
|
||||||
|
<Frame>
|
||||||
|
<img src="/images/current-weather.png" />
|
||||||
|
</Frame>
|
||||||
|
<Frame>
|
||||||
|
<img src="/images/weather-alerts.png" />
|
||||||
</Frame>
|
</Frame>
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
|
|
@ -445,4 +988,79 @@ When you ask a question:
|
||||||
5. Claude formulates a natural language response
|
5. Claude formulates a natural language response
|
||||||
6. The response is displayed to you!
|
6. The response is displayed to you!
|
||||||
|
|
||||||
## Next steps
|
## Troubleshooting
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Weather API Issues">
|
||||||
|
**Error: Failed to retrieve grid point data**
|
||||||
|
|
||||||
|
This usually means either:
|
||||||
|
1. The coordinates are outside the US
|
||||||
|
2. The NWS API is having issues
|
||||||
|
3. You're being rate limited
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Verify you're using US coordinates
|
||||||
|
- Add a small delay between requests
|
||||||
|
- Check the NWS API status page
|
||||||
|
|
||||||
|
**Error: No active alerts for [STATE]**
|
||||||
|
|
||||||
|
This isn't an error - it just means there are no current weather alerts for that state. Try a different state or check during severe weather.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Claude Desktop Integration Issues">
|
||||||
|
**Server not showing up in Claude**
|
||||||
|
1. Check your configuration file syntax
|
||||||
|
2. Make sure the path to your project is correct
|
||||||
|
3. Restart Claude Desktop completely
|
||||||
|
|
||||||
|
You can also check Claude's logs for errors like so:
|
||||||
|
```bash
|
||||||
|
# Check Claude's logs for errors
|
||||||
|
tail -f ~/Library/Application\ Support/Claude/Claude.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tool calls failing silently**
|
||||||
|
|
||||||
|
If Claude attempts to use the tools but they fail:
|
||||||
|
|
||||||
|
1. Check Claude's logs for errors
|
||||||
|
2. Verify your server runs without errors
|
||||||
|
3. Try restarting Claude Desktop
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card
|
||||||
|
title="Building a client"
|
||||||
|
icon="outlet"
|
||||||
|
href="/tutorials/building-a-client"
|
||||||
|
>
|
||||||
|
Learn how to build your first MCP client
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title="Building a local fileserver"
|
||||||
|
icon="files"
|
||||||
|
href="/examples"
|
||||||
|
>
|
||||||
|
Build an intermediate MCP server that communicates with your local filesystem
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title="Example servers"
|
||||||
|
icon="grid"
|
||||||
|
href="/examples"
|
||||||
|
>
|
||||||
|
Check out our gallery of official MCP servers and implementations
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title="Core architecture"
|
||||||
|
icon="sitemap"
|
||||||
|
href="/docs/concepts/architecture"
|
||||||
|
>
|
||||||
|
Understand how MCP connects clients, servers, and LLMs
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
Loading…
Reference in a new issue