Fixed
This commit is contained in:
parent
9109d03fa6
commit
7d6754d094
2 changed files with 0 additions and 1427 deletions
|
|
@ -1,828 +0,0 @@
|
||||||
---
|
|
||||||
title: "Python"
|
|
||||||
description: "Create a simple MCP server in Python in 15 minutes"
|
|
||||||
---
|
|
||||||
|
|
||||||
Let's build your first MCP server in Python! We'll create a weather server that provides current weather data as a resource and lets Claude fetch forecasts using tools.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
This guide uses the OpenWeatherMap API. You'll need a free API key from [OpenWeatherMap](https://openweathermap.org/api) to follow along.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
<Info>
|
|
||||||
The following steps are for macOS. Guides for other platforms are coming soon.
|
|
||||||
</Info>
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Install Python">
|
|
||||||
You'll need Python 3.10 or higher:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python --version # Should be 3.10 or higher
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Install uv via homebrew">
|
|
||||||
See https://docs.astral.sh/uv/ for more information.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install uv
|
|
||||||
uv --version # Should be 0.4.18 or higher
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Create a new project using the MCP project creator">
|
|
||||||
```bash
|
|
||||||
uvx create-mcp-server --path weather_service
|
|
||||||
cd weather_service
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Install additional dependencies">
|
|
||||||
```bash
|
|
||||||
uv add httpx python-dotenv
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Set up environment">
|
|
||||||
Create `.env`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
OPENWEATHER_API_KEY=your-api-key-here
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
## Create your server
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Add the base imports and setup">
|
|
||||||
In `weather_service/src/weather_service/server.py`
|
|
||||||
```python
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from collections.abc import Sequence
|
|
||||||
from functools import lru_cache
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
import asyncio
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from mcp.server import Server
|
|
||||||
from mcp.types import (
|
|
||||||
Resource,
|
|
||||||
Tool,
|
|
||||||
TextContent,
|
|
||||||
ImageContent,
|
|
||||||
EmbeddedResource,
|
|
||||||
LoggingLevel
|
|
||||||
)
|
|
||||||
from pydantic import AnyUrl
|
|
||||||
|
|
||||||
# Load environment variables
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger("weather-server")
|
|
||||||
|
|
||||||
# API configuration
|
|
||||||
API_KEY = os.getenv("OPENWEATHER_API_KEY")
|
|
||||||
if not API_KEY:
|
|
||||||
raise ValueError("OPENWEATHER_API_KEY environment variable required")
|
|
||||||
|
|
||||||
API_BASE_URL = "http://api.openweathermap.org/data/2.5"
|
|
||||||
DEFAULT_CITY = "London"
|
|
||||||
CURRENT_WEATHER_ENDPOINT = "weather"
|
|
||||||
FORECAST_ENDPOINT = "forecast"
|
|
||||||
|
|
||||||
# The rest of our server implementation will go here
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Add weather fetching functionality">
|
|
||||||
Add this functionality:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Create reusable params
|
|
||||||
http_params = {
|
|
||||||
"appid": API_KEY,
|
|
||||||
"units": "metric"
|
|
||||||
}
|
|
||||||
|
|
||||||
async def fetch_weather(city: str) -> dict[str, Any]:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(
|
|
||||||
f"{API_BASE_URL}/weather",
|
|
||||||
params={"q": city, **http_params}
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"temperature": data["main"]["temp"],
|
|
||||||
"conditions": data["weather"][0]["description"],
|
|
||||||
"humidity": data["main"]["humidity"],
|
|
||||||
"wind_speed": data["wind"]["speed"],
|
|
||||||
"timestamp": datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
app = Server("weather-server")
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Implement resource handlers">
|
|
||||||
Add these resource-related handlers to our main function:
|
|
||||||
|
|
||||||
```python
|
|
||||||
app = Server("weather-server")
|
|
||||||
|
|
||||||
@app.list_resources()
|
|
||||||
async def list_resources() -> list[Resource]:
|
|
||||||
"""List available weather resources."""
|
|
||||||
uri = AnyUrl(f"weather://{DEFAULT_CITY}/current")
|
|
||||||
return [
|
|
||||||
Resource(
|
|
||||||
uri=uri,
|
|
||||||
name=f"Current weather in {DEFAULT_CITY}",
|
|
||||||
mimeType="application/json",
|
|
||||||
description="Real-time weather data"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
@app.read_resource()
|
|
||||||
async def read_resource(uri: AnyUrl) -> str:
|
|
||||||
"""Read current weather data for a city."""
|
|
||||||
city = DEFAULT_CITY
|
|
||||||
if str(uri).startswith("weather://") and str(uri).endswith("/current"):
|
|
||||||
city = str(uri).split("/")[-2]
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown resource: {uri}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
weather_data = await fetch_weather(city)
|
|
||||||
return json.dumps(weather_data, indent=2)
|
|
||||||
except httpx.HTTPError as e:
|
|
||||||
raise RuntimeError(f"Weather API error: {str(e)}")
|
|
||||||
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Implement tool handlers">
|
|
||||||
Add these tool-related handlers:
|
|
||||||
|
|
||||||
```python
|
|
||||||
app = Server("weather-server")
|
|
||||||
|
|
||||||
# Resource implementation ...
|
|
||||||
|
|
||||||
@app.list_tools()
|
|
||||||
async def list_tools() -> list[Tool]:
|
|
||||||
"""List available weather tools."""
|
|
||||||
return [
|
|
||||||
Tool(
|
|
||||||
name="get_forecast",
|
|
||||||
description="Get weather forecast for a city",
|
|
||||||
inputSchema={
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"city": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "City name"
|
|
||||||
},
|
|
||||||
"days": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Number of days (1-5)",
|
|
||||||
"minimum": 1,
|
|
||||||
"maximum": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["city"]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
@app.call_tool()
|
|
||||||
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
|
|
||||||
"""Handle tool calls for weather forecasts."""
|
|
||||||
if name != "get_forecast":
|
|
||||||
raise ValueError(f"Unknown tool: {name}")
|
|
||||||
|
|
||||||
if not isinstance(arguments, dict) or "city" not in arguments:
|
|
||||||
raise ValueError("Invalid forecast arguments")
|
|
||||||
|
|
||||||
city = arguments["city"]
|
|
||||||
days = min(int(arguments.get("days", 3)), 5)
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(
|
|
||||||
f"{API_BASE_URL}/{FORECAST_ENDPOINT}",
|
|
||||||
params={
|
|
||||||
"q": city,
|
|
||||||
"cnt": days * 8, # API returns 3-hour intervals
|
|
||||||
**http_params,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
forecasts = []
|
|
||||||
for i in range(0, len(data["list"]), 8):
|
|
||||||
day_data = data["list"][i]
|
|
||||||
forecasts.append({
|
|
||||||
"date": day_data["dt_txt"].split()[0],
|
|
||||||
"temperature": day_data["main"]["temp"],
|
|
||||||
"conditions": day_data["weather"][0]["description"]
|
|
||||||
})
|
|
||||||
|
|
||||||
return [
|
|
||||||
TextContent(
|
|
||||||
type="text",
|
|
||||||
text=json.dumps(forecasts, indent=2)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
except httpx.HTTPError as e:
|
|
||||||
logger.error(f"Weather API error: {str(e)}")
|
|
||||||
raise RuntimeError(f"Weather API error: {str(e)}")
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Add the main function">
|
|
||||||
Add this to the end of `weather_service/src/weather_service/server.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def main():
|
|
||||||
# Import here to avoid issues with event loops
|
|
||||||
from mcp.server.stdio import stdio_server
|
|
||||||
|
|
||||||
async with stdio_server() as (read_stream, write_stream):
|
|
||||||
await app.run(
|
|
||||||
read_stream,
|
|
||||||
write_stream,
|
|
||||||
app.create_initialization_options()
|
|
||||||
)
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Check your entry point in __init__.py">
|
|
||||||
Add this to the end of `weather_service/src/weather_service/__init__.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from . import server
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main entry point for the package."""
|
|
||||||
asyncio.run(server.main())
|
|
||||||
|
|
||||||
# Optionally expose other important items at package level
|
|
||||||
__all__ = ['main', 'server']
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Connect to Claude Desktop
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Update Claude config">
|
|
||||||
Add to `claude_desktop_config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"weather": {
|
|
||||||
"command": "uv",
|
|
||||||
"args": [
|
|
||||||
"--directory",
|
|
||||||
"path/to/your/project",
|
|
||||||
"run",
|
|
||||||
"weather-service"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"OPENWEATHER_API_KEY": "your-api-key"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Restart Claude">
|
|
||||||
1. Quit Claude completely
|
|
||||||
|
|
||||||
2. Start Claude again
|
|
||||||
|
|
||||||
3. Look for your weather server in the 🔌 menu
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
## Try it out!
|
|
||||||
|
|
||||||
<AccordionGroup>
|
|
||||||
<Accordion title="Check Current Weather" active>
|
|
||||||
Ask Claude:
|
|
||||||
|
|
||||||
```
|
|
||||||
What's the current weather in San Francisco? Can you analyze the conditions and tell me if it's a good day for outdoor activities?
|
|
||||||
```
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="Get a Forecast">
|
|
||||||
Ask Claude:
|
|
||||||
|
|
||||||
```
|
|
||||||
Can you get me a 5-day forecast for Tokyo and help me plan what clothes to pack for my trip?
|
|
||||||
```
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="Compare Weather">
|
|
||||||
Ask Claude:
|
|
||||||
|
|
||||||
```
|
|
||||||
Can you analyze the forecast for both Tokyo and San Francisco and tell me which city would be better for outdoor photography this week?
|
|
||||||
```
|
|
||||||
</Accordion>
|
|
||||||
</AccordionGroup>
|
|
||||||
|
|
||||||
## Understanding the code
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<Tab title="Type Hints">
|
|
||||||
```python
|
|
||||||
async def read_resource(self, uri: str) -> ReadResourceResult:
|
|
||||||
# ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Python type hints help catch errors early and improve code maintainability.
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab title="Resources">
|
|
||||||
```python
|
|
||||||
@app.list_resources()
|
|
||||||
async def list_resources(self) -> ListResourcesResult:
|
|
||||||
return ListResourcesResult(
|
|
||||||
resources=[
|
|
||||||
Resource(
|
|
||||||
uri=f"weather://{DEFAULT_CITY}/current",
|
|
||||||
name=f"Current weather in {DEFAULT_CITY}",
|
|
||||||
mimeType="application/json",
|
|
||||||
description="Real-time weather data"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Resources provide data that Claude can access as context.
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab title="Tools">
|
|
||||||
```python
|
|
||||||
Tool(
|
|
||||||
name="get_forecast",
|
|
||||||
description="Get weather forecast for a city",
|
|
||||||
inputSchema={
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"city": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "City name"
|
|
||||||
},
|
|
||||||
"days": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Number of days (1-5)",
|
|
||||||
"minimum": 1,
|
|
||||||
"maximum": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["city"]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Tools let Claude take actions through your server with validated inputs.
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab title="Server Structure">
|
|
||||||
```python
|
|
||||||
# Create server instance with name
|
|
||||||
app = Server("weather-server")
|
|
||||||
|
|
||||||
# Register resource handler
|
|
||||||
@app.list_resources()
|
|
||||||
async def list_resources() -> list[Resource]:
|
|
||||||
"""List available resources"""
|
|
||||||
return [...]
|
|
||||||
|
|
||||||
# Register tool handler
|
|
||||||
@app.call_tool()
|
|
||||||
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
|
|
||||||
"""Handle tool execution"""
|
|
||||||
return [...]
|
|
||||||
|
|
||||||
# Register additional handlers
|
|
||||||
@app.read_resource()
|
|
||||||
...
|
|
||||||
@app.list_tools()
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
The MCP server uses a simple app pattern - create a Server instance and register handlers with decorators. Each handler maps to a specific MCP protocol operation.
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
## Best practices
|
|
||||||
|
|
||||||
<CardGroup cols={1}>
|
|
||||||
<Card title="Error Handling" icon="shield">
|
|
||||||
```python
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(..., params={..., **http_params})
|
|
||||||
response.raise_for_status()
|
|
||||||
except httpx.HTTPError as e:
|
|
||||||
raise McpError(
|
|
||||||
ErrorCode.INTERNAL_ERROR,
|
|
||||||
f"API error: {str(e)}"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card title="Type Validation" icon="check">
|
|
||||||
```python
|
|
||||||
if not isinstance(args, dict) or "city" not in args:
|
|
||||||
raise McpError(
|
|
||||||
ErrorCode.INVALID_PARAMS,
|
|
||||||
"Invalid forecast arguments"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card title="Environment Variables" icon="gear">
|
|
||||||
```python
|
|
||||||
if not API_KEY:
|
|
||||||
raise ValueError("OPENWEATHER_API_KEY is required")
|
|
||||||
```
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
</CardGroup>
|
|
||||||
|
|
||||||
## Available transports
|
|
||||||
|
|
||||||
While this guide uses stdio transport, MCP supports additional transport options:
|
|
||||||
|
|
||||||
### SSE (Server-Sent Events)
|
|
||||||
|
|
||||||
```python
|
|
||||||
from mcp.server.sse import SseServerTransport
|
|
||||||
from starlette.applications import Starlette
|
|
||||||
from starlette.routing import Route
|
|
||||||
|
|
||||||
# Create SSE transport with endpoint
|
|
||||||
sse = SseServerTransport("/messages")
|
|
||||||
|
|
||||||
# Handler for SSE connections
|
|
||||||
async def handle_sse(scope, receive, send):
|
|
||||||
async with sse.connect_sse(scope, receive, send) as streams:
|
|
||||||
await app.run(
|
|
||||||
streams[0], streams[1], app.create_initialization_options()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Handler for client messages
|
|
||||||
async def handle_messages(scope, receive, send):
|
|
||||||
await sse.handle_post_message(scope, receive, send)
|
|
||||||
|
|
||||||
# Create Starlette app with routes
|
|
||||||
app = Starlette(
|
|
||||||
debug=True,
|
|
||||||
routes=[
|
|
||||||
Route("/sse", endpoint=handle_sse),
|
|
||||||
Route("/messages", endpoint=handle_messages, methods=["POST"]),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Run with any ASGI server
|
|
||||||
import uvicorn
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advanced features
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Understanding Request Context">
|
|
||||||
The request context provides access to the current request's metadata and the active client session. Access it through `server.request_context`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
@app.call_tool()
|
|
||||||
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
|
|
||||||
# Access the current request context
|
|
||||||
ctx = self.request_context
|
|
||||||
|
|
||||||
# Get request metadata like progress tokens
|
|
||||||
if progress_token := ctx.meta.progressToken:
|
|
||||||
# Send progress notifications via the session
|
|
||||||
await ctx.session.send_progress_notification(
|
|
||||||
progress_token=progress_token,
|
|
||||||
progress=0.5,
|
|
||||||
total=1.0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sample from the LLM client
|
|
||||||
result = await ctx.session.create_message(
|
|
||||||
messages=[
|
|
||||||
SamplingMessage(
|
|
||||||
role="user",
|
|
||||||
content=TextContent(
|
|
||||||
type="text",
|
|
||||||
text="Analyze this weather data: " + json.dumps(arguments)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
max_tokens=100
|
|
||||||
)
|
|
||||||
|
|
||||||
return [TextContent(type="text", text=result.content.text)]
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Add caching">
|
|
||||||
```python
|
|
||||||
# Cache settings
|
|
||||||
cache_timeout = timedelta(minutes=15)
|
|
||||||
last_cache_time = None
|
|
||||||
cached_weather = None
|
|
||||||
|
|
||||||
async def fetch_weather(city: str) -> dict[str, Any]:
|
|
||||||
global cached_weather, last_cache_time
|
|
||||||
|
|
||||||
now = datetime.now()
|
|
||||||
if (cached_weather is None or
|
|
||||||
last_cache_time is None or
|
|
||||||
now - last_cache_time > cache_timeout):
|
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(
|
|
||||||
f"{API_BASE_URL}/{CURRENT_WEATHER_ENDPOINT}",
|
|
||||||
params={"q": city, **http_params}
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
cached_weather = {
|
|
||||||
"temperature": data["main"]["temp"],
|
|
||||||
"conditions": data["weather"][0]["description"],
|
|
||||||
"humidity": data["main"]["humidity"],
|
|
||||||
"wind_speed": data["wind"]["speed"],
|
|
||||||
"timestamp": datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
last_cache_time = now
|
|
||||||
|
|
||||||
return cached_weather
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Add progress notifications">
|
|
||||||
```python
|
|
||||||
@self.call_tool()
|
|
||||||
async def call_tool(self, name: str, arguments: Any) -> CallToolResult:
|
|
||||||
if progress_token := self.request_context.meta.progressToken:
|
|
||||||
# Send progress notifications
|
|
||||||
await self.request_context.session.send_progress_notification(
|
|
||||||
progress_token=progress_token,
|
|
||||||
progress=1,
|
|
||||||
total=2
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fetch data...
|
|
||||||
|
|
||||||
await self.request_context.session.send_progress_notification(
|
|
||||||
progress_token=progress_token,
|
|
||||||
progress=2,
|
|
||||||
total=2
|
|
||||||
)
|
|
||||||
|
|
||||||
# Rest of the method implementation...
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Add logging support">
|
|
||||||
```python
|
|
||||||
# Set up logging
|
|
||||||
logger = logging.getLogger("weather-server")
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
@app.set_logging_level()
|
|
||||||
async def set_logging_level(level: LoggingLevel) -> EmptyResult:
|
|
||||||
logger.setLevel(level.upper())
|
|
||||||
await app.request_context.session.send_log_message(
|
|
||||||
level="info",
|
|
||||||
data=f"Log level set to {level}",
|
|
||||||
logger="weather-server"
|
|
||||||
)
|
|
||||||
return EmptyResult()
|
|
||||||
|
|
||||||
# Use logger throughout the code
|
|
||||||
# For example:
|
|
||||||
# logger.info("Weather data fetched successfully")
|
|
||||||
# logger.error(f"Error fetching weather data: {str(e)}")
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Add resource templates">
|
|
||||||
```python
|
|
||||||
@app.list_resource_templates()
|
|
||||||
async def list_resource_templates() -> list[ResourceTemplate]:
|
|
||||||
return [
|
|
||||||
ResourceTemplate(
|
|
||||||
uriTemplate="weather://{city}/current",
|
|
||||||
name="Current weather for any city",
|
|
||||||
mimeType="application/json"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Create test file">
|
|
||||||
Create `tests/weather_test.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import pytest
|
|
||||||
import os
|
|
||||||
from unittest.mock import patch, Mock
|
|
||||||
from datetime import datetime
|
|
||||||
import json
|
|
||||||
from pydantic import AnyUrl
|
|
||||||
os.environ["OPENWEATHER_API_KEY"] = "TEST"
|
|
||||||
|
|
||||||
from weather_service.server import (
|
|
||||||
fetch_weather,
|
|
||||||
read_resource,
|
|
||||||
call_tool,
|
|
||||||
list_resources,
|
|
||||||
list_tools,
|
|
||||||
DEFAULT_CITY
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def anyio_backend():
|
|
||||||
return "asyncio"
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_weather_response():
|
|
||||||
return {
|
|
||||||
"main": {
|
|
||||||
"temp": 20.5,
|
|
||||||
"humidity": 65
|
|
||||||
},
|
|
||||||
"weather": [
|
|
||||||
{"description": "scattered clouds"}
|
|
||||||
],
|
|
||||||
"wind": {
|
|
||||||
"speed": 3.6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_forecast_response():
|
|
||||||
return {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"dt_txt": "2024-01-01 12:00:00",
|
|
||||||
"main": {"temp": 18.5},
|
|
||||||
"weather": [{"description": "sunny"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dt_txt": "2024-01-02 12:00:00",
|
|
||||||
"main": {"temp": 17.2},
|
|
||||||
"weather": [{"description": "cloudy"}]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_fetch_weather(mock_weather_response):
|
|
||||||
with patch('requests.Session.get') as mock_get:
|
|
||||||
mock_get.return_value.json.return_value = mock_weather_response
|
|
||||||
mock_get.return_value.raise_for_status = Mock()
|
|
||||||
|
|
||||||
weather = await fetch_weather("London")
|
|
||||||
|
|
||||||
assert weather["temperature"] == 20.5
|
|
||||||
assert weather["conditions"] == "scattered clouds"
|
|
||||||
assert weather["humidity"] == 65
|
|
||||||
assert weather["wind_speed"] == 3.6
|
|
||||||
assert "timestamp" in weather
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_read_resource():
|
|
||||||
with patch('weather_service.server.fetch_weather') as mock_fetch:
|
|
||||||
mock_fetch.return_value = {
|
|
||||||
"temperature": 20.5,
|
|
||||||
"conditions": "clear sky",
|
|
||||||
"timestamp": datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
uri = AnyUrl("weather://London/current")
|
|
||||||
result = await read_resource(uri)
|
|
||||||
|
|
||||||
assert isinstance(result, str)
|
|
||||||
assert "temperature" in result
|
|
||||||
assert "clear sky" in result
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_call_tool(mock_forecast_response):
|
|
||||||
class Response():
|
|
||||||
def raise_for_status(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def json(self):
|
|
||||||
return mock_forecast_response
|
|
||||||
|
|
||||||
class AsyncClient():
|
|
||||||
async def __aenter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __aexit__(self, *exc_info):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def get(self, *args, **kwargs):
|
|
||||||
return Response()
|
|
||||||
|
|
||||||
with patch('httpx.AsyncClient', new=AsyncClient) as mock_client:
|
|
||||||
result = await call_tool("get_forecast", {"city": "London", "days": 2})
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
assert result[0].type == "text"
|
|
||||||
forecast_data = json.loads(result[0].text)
|
|
||||||
assert len(forecast_data) == 1
|
|
||||||
assert forecast_data[0]["temperature"] == 18.5
|
|
||||||
assert forecast_data[0]["conditions"] == "sunny"
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_list_resources():
|
|
||||||
resources = await list_resources()
|
|
||||||
assert len(resources) == 1
|
|
||||||
assert resources[0].name == f"Current weather in {DEFAULT_CITY}"
|
|
||||||
assert resources[0].mimeType == "application/json"
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_list_tools():
|
|
||||||
tools = await list_tools()
|
|
||||||
assert len(tools) == 1
|
|
||||||
assert tools[0].name == "get_forecast"
|
|
||||||
assert "city" in tools[0].inputSchema["properties"]
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
<Step title="Run tests">
|
|
||||||
```bash
|
|
||||||
uv add --dev pytest
|
|
||||||
uv run pytest
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Installation issues
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check Python version
|
|
||||||
python --version
|
|
||||||
|
|
||||||
# Reinstall dependencies
|
|
||||||
uv sync --reinstall
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type checking
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install mypy
|
|
||||||
uv add --dev pyright
|
|
||||||
|
|
||||||
# Run type checker
|
|
||||||
uv run pyright src
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next steps
|
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
|
||||||
<Card title="Architecture overview" icon="sitemap" href="/docs/concepts/architecture">
|
|
||||||
Learn more about the MCP architecture
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card title="Python SDK" icon="python" href="https://github.com/modelcontextprotocol/python-sdk">
|
|
||||||
Check out the Python SDK on GitHub
|
|
||||||
</Card>
|
|
||||||
</CardGroup>
|
|
||||||
|
|
@ -1,599 +0,0 @@
|
||||||
---
|
|
||||||
title: "TypeScript"
|
|
||||||
description: "Create a simple MCP server in TypeScript in 15 minutes"
|
|
||||||
---
|
|
||||||
|
|
||||||
Let's build your first MCP server in TypeScript! We'll create a weather server that provides current weather data as a resource and lets Claude fetch forecasts using tools.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
This guide uses the OpenWeatherMap API. You'll need a free API key from [OpenWeatherMap](https://openweathermap.org/api) to follow along.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Install Node.js">
|
|
||||||
You'll need Node.js 18 or higher:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node --version # Should be v18 or higher
|
|
||||||
npm --version
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Create a new project">
|
|
||||||
You can use our [create-typescript-server](https://github.com/modelcontextprotocol/create-typescript-server) tool to bootstrap a new project:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx @modelcontextprotocol/create-server weather-server
|
|
||||||
cd weather-server
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Install dependencies">
|
|
||||||
```bash
|
|
||||||
npm install --save axios dotenv
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Set up environment">
|
|
||||||
Create `.env`:
|
|
||||||
```bash
|
|
||||||
OPENWEATHER_API_KEY=your-api-key-here
|
|
||||||
```
|
|
||||||
Make sure to add your environment file to `.gitignore`
|
|
||||||
```bash
|
|
||||||
.env
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
## Create your server
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Define types">
|
|
||||||
Create a file `src/types.ts`, and add the following:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface OpenWeatherResponse {
|
|
||||||
main: {
|
|
||||||
temp: number;
|
|
||||||
humidity: number;
|
|
||||||
};
|
|
||||||
weather: Array<{
|
|
||||||
description: string;
|
|
||||||
}>;
|
|
||||||
wind: {
|
|
||||||
speed: number;
|
|
||||||
};
|
|
||||||
dt_txt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WeatherData {
|
|
||||||
temperature: number;
|
|
||||||
conditions: string;
|
|
||||||
humidity: number;
|
|
||||||
wind_speed: number;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ForecastDay {
|
|
||||||
date: string;
|
|
||||||
temperature: number;
|
|
||||||
conditions: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GetForecastArgs {
|
|
||||||
city: string;
|
|
||||||
days?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type guard for forecast arguments
|
|
||||||
export function isValidForecastArgs(args: any): args is GetForecastArgs {
|
|
||||||
return (
|
|
||||||
typeof args === "object" &&
|
|
||||||
args !== null &&
|
|
||||||
"city" in args &&
|
|
||||||
typeof args.city === "string" &&
|
|
||||||
(args.days === undefined || typeof args.days === "number")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Add the base code">
|
|
||||||
Replace `src/index.ts` with the following:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
#!/usr/bin/env node
|
|
||||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
||||||
import {
|
|
||||||
ListResourcesRequestSchema,
|
|
||||||
ReadResourceRequestSchema,
|
|
||||||
ListToolsRequestSchema,
|
|
||||||
CallToolRequestSchema,
|
|
||||||
ErrorCode,
|
|
||||||
McpError
|
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
|
||||||
import axios from "axios";
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
import {
|
|
||||||
WeatherData,
|
|
||||||
ForecastDay,
|
|
||||||
OpenWeatherResponse,
|
|
||||||
isValidForecastArgs
|
|
||||||
} from "./types.js";
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const API_KEY = process.env.OPENWEATHER_API_KEY;
|
|
||||||
if (!API_KEY) {
|
|
||||||
throw new Error("OPENWEATHER_API_KEY environment variable is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_CONFIG = {
|
|
||||||
BASE_URL: 'http://api.openweathermap.org/data/2.5',
|
|
||||||
DEFAULT_CITY: 'San Francisco',
|
|
||||||
ENDPOINTS: {
|
|
||||||
CURRENT: 'weather',
|
|
||||||
FORECAST: 'forecast'
|
|
||||||
}
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
class WeatherServer {
|
|
||||||
private server: Server;
|
|
||||||
private axiosInstance;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.server = new Server({
|
|
||||||
name: "example-weather-server",
|
|
||||||
version: "0.1.0"
|
|
||||||
}, {
|
|
||||||
capabilities: {
|
|
||||||
resources: {},
|
|
||||||
tools: {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure axios with defaults
|
|
||||||
this.axiosInstance = axios.create({
|
|
||||||
baseURL: API_CONFIG.BASE_URL,
|
|
||||||
params: {
|
|
||||||
appid: API_KEY,
|
|
||||||
units: "metric"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setupHandlers();
|
|
||||||
this.setupErrorHandling();
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupErrorHandling(): void {
|
|
||||||
this.server.onerror = (error) => {
|
|
||||||
console.error("[MCP Error]", error);
|
|
||||||
};
|
|
||||||
|
|
||||||
process.on('SIGINT', async () => {
|
|
||||||
await this.server.close();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupHandlers(): void {
|
|
||||||
this.setupResourceHandlers();
|
|
||||||
this.setupToolHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupResourceHandlers(): void {
|
|
||||||
// Implementation continues in next section
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupToolHandlers(): void {
|
|
||||||
// Implementation continues in next section
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(): Promise<void> {
|
|
||||||
const transport = new StdioServerTransport();
|
|
||||||
await this.server.connect(transport);
|
|
||||||
|
|
||||||
// Although this is just an informative message, we must log to stderr,
|
|
||||||
// to avoid interfering with MCP communication that happens on stdout
|
|
||||||
console.error("Weather MCP server running on stdio");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = new WeatherServer();
|
|
||||||
server.run().catch(console.error);
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Add resource handlers">
|
|
||||||
Add this to the `setupResourceHandlers` method:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
private setupResourceHandlers(): void {
|
|
||||||
this.server.setRequestHandler(
|
|
||||||
ListResourcesRequestSchema,
|
|
||||||
async () => ({
|
|
||||||
resources: [{
|
|
||||||
uri: `weather://${API_CONFIG.DEFAULT_CITY}/current`,
|
|
||||||
name: `Current weather in ${API_CONFIG.DEFAULT_CITY}`,
|
|
||||||
mimeType: "application/json",
|
|
||||||
description: "Real-time weather data including temperature, conditions, humidity, and wind speed"
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.server.setRequestHandler(
|
|
||||||
ReadResourceRequestSchema,
|
|
||||||
async (request) => {
|
|
||||||
const city = API_CONFIG.DEFAULT_CITY;
|
|
||||||
if (request.params.uri !== `weather://${city}/current`) {
|
|
||||||
throw new McpError(
|
|
||||||
ErrorCode.InvalidRequest,
|
|
||||||
`Unknown resource: ${request.params.uri}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.axiosInstance.get<OpenWeatherResponse>(
|
|
||||||
API_CONFIG.ENDPOINTS.CURRENT,
|
|
||||||
{
|
|
||||||
params: { q: city }
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const weatherData: WeatherData = {
|
|
||||||
temperature: response.data.main.temp,
|
|
||||||
conditions: response.data.weather[0].description,
|
|
||||||
humidity: response.data.main.humidity,
|
|
||||||
wind_speed: response.data.wind.speed,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
contents: [{
|
|
||||||
uri: request.params.uri,
|
|
||||||
mimeType: "application/json",
|
|
||||||
text: JSON.stringify(weatherData, null, 2)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
throw new McpError(
|
|
||||||
ErrorCode.InternalError,
|
|
||||||
`Weather API error: ${error.response?.data.message ?? error.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Add tool handlers">
|
|
||||||
Add these handlers to the `setupToolHandlers` method:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
private setupToolHandlers(): void {
|
|
||||||
this.server.setRequestHandler(
|
|
||||||
ListToolsRequestSchema,
|
|
||||||
async () => ({
|
|
||||||
tools: [{
|
|
||||||
name: "get_forecast",
|
|
||||||
description: "Get weather forecast for a city",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
city: {
|
|
||||||
type: "string",
|
|
||||||
description: "City name"
|
|
||||||
},
|
|
||||||
days: {
|
|
||||||
type: "number",
|
|
||||||
description: "Number of days (1-5)",
|
|
||||||
minimum: 1,
|
|
||||||
maximum: 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ["city"]
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.server.setRequestHandler(
|
|
||||||
CallToolRequestSchema,
|
|
||||||
async (request) => {
|
|
||||||
if (request.params.name !== "get_forecast") {
|
|
||||||
throw new McpError(
|
|
||||||
ErrorCode.MethodNotFound,
|
|
||||||
`Unknown tool: ${request.params.name}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValidForecastArgs(request.params.arguments)) {
|
|
||||||
throw new McpError(
|
|
||||||
ErrorCode.InvalidParams,
|
|
||||||
"Invalid forecast arguments"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const city = request.params.arguments.city;
|
|
||||||
const days = Math.min(request.params.arguments.days || 3, 5);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.axiosInstance.get<{
|
|
||||||
list: OpenWeatherResponse[]
|
|
||||||
}>(API_CONFIG.ENDPOINTS.FORECAST, {
|
|
||||||
params: {
|
|
||||||
q: city,
|
|
||||||
cnt: days * 8 // API returns 3-hour intervals
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const forecasts: ForecastDay[] = [];
|
|
||||||
for (let i = 0; i < response.data.list.length; i += 8) {
|
|
||||||
const dayData = response.data.list[i];
|
|
||||||
forecasts.push({
|
|
||||||
date: dayData.dt_txt?.split(' ')[0] ?? new Date().toISOString().split('T')[0],
|
|
||||||
temperature: dayData.main.temp,
|
|
||||||
conditions: dayData.weather[0].description
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(forecasts, null, 2)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
return {
|
|
||||||
content: [{
|
|
||||||
type: "text",
|
|
||||||
text: `Weather API error: ${error.response?.data.message ?? error.message}`
|
|
||||||
}],
|
|
||||||
isError: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Build and test">
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
## Connect to Claude Desktop
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Update Claude config">
|
|
||||||
If you didn't already connect to Claude Desktop during project setup, add to `claude_desktop_config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"weather": {
|
|
||||||
"command": "node",
|
|
||||||
"args": ["/path/to/weather-server/build/index.js"],
|
|
||||||
"env": {
|
|
||||||
"OPENWEATHER_API_KEY": "your-api-key",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title="Restart Claude">
|
|
||||||
1. Quit Claude completely
|
|
||||||
2. Start Claude again
|
|
||||||
3. Look for your weather server in the 🔌 menu
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
## Try it out!
|
|
||||||
|
|
||||||
<AccordionGroup>
|
|
||||||
<Accordion title="Check Current Weather" active>
|
|
||||||
Ask Claude:
|
|
||||||
```
|
|
||||||
What's the current weather in San Francisco? Can you analyze the conditions?
|
|
||||||
```
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="Get a Forecast">
|
|
||||||
Ask Claude:
|
|
||||||
```
|
|
||||||
Can you get me a 5-day forecast for Tokyo and tell me if I should pack an umbrella?
|
|
||||||
```
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="Compare Weather">
|
|
||||||
Ask Claude:
|
|
||||||
```
|
|
||||||
Can you analyze the forecast for both Tokyo and San Francisco and tell me which city will be warmer this week?
|
|
||||||
```
|
|
||||||
</Accordion>
|
|
||||||
</AccordionGroup>
|
|
||||||
|
|
||||||
## Understanding the code
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<Tab title="Type Safety">
|
|
||||||
```typescript
|
|
||||||
interface WeatherData {
|
|
||||||
temperature: number;
|
|
||||||
conditions: string;
|
|
||||||
humidity: number;
|
|
||||||
wind_speed: number;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
TypeScript adds type safety to our MCP server, making it more reliable and easier to maintain.
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab title="Resources">
|
|
||||||
```typescript
|
|
||||||
this.server.setRequestHandler(
|
|
||||||
ListResourcesRequestSchema,
|
|
||||||
async () => ({
|
|
||||||
resources: [{
|
|
||||||
uri: `weather://${DEFAULT_CITY}/current`,
|
|
||||||
name: `Current weather in ${DEFAULT_CITY}`,
|
|
||||||
mimeType: "application/json"
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
```
|
|
||||||
Resources provide data that Claude can access as context.
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab title="Tools">
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
name: "get_forecast",
|
|
||||||
description: "Get weather forecast for a city",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
city: { type: "string" },
|
|
||||||
days: { type: "number" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Tools let Claude take actions through your server with type-safe inputs.
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
## Best practices
|
|
||||||
|
|
||||||
<CardGroup cols={1}>
|
|
||||||
<Card
|
|
||||||
title="Error Handling"
|
|
||||||
icon="shield"
|
|
||||||
>
|
|
||||||
When a tool encounters an error, return the error message with `isError: true`, so the model can self-correct:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
const response = await axiosInstance.get(...);
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
return {
|
|
||||||
content: {
|
|
||||||
mimeType: "text/plain",
|
|
||||||
text: `Weather API error: ${error.response?.data.message ?? error.message}`
|
|
||||||
},
|
|
||||||
isError: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For other handlers, throw an error, so the application can notify the user:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
const response = await this.axiosInstance.get(...);
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
throw new McpError(
|
|
||||||
ErrorCode.InternalError,
|
|
||||||
`Weather API error: ${error.response?.data.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
title="Type Validation"
|
|
||||||
icon="check"
|
|
||||||
>
|
|
||||||
```typescript
|
|
||||||
function isValidForecastArgs(args: any): args is GetForecastArgs {
|
|
||||||
return (
|
|
||||||
typeof args === "object" &&
|
|
||||||
args !== null &&
|
|
||||||
"city" in args &&
|
|
||||||
typeof args.city === "string"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<Tip>You can also use libraries like [Zod](https://zod.dev/) to perform this validation automatically.</Tip>
|
|
||||||
</Card>
|
|
||||||
</CardGroup>
|
|
||||||
|
|
||||||
## Available transports
|
|
||||||
|
|
||||||
While this guide uses stdio to run the MCP server as a local process, MCP supports other [transports](/docs/concepts/transports) as well.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
<Info>
|
|
||||||
The following troubleshooting tips are for macOS. Guides for other platforms are coming soon.
|
|
||||||
</Info>
|
|
||||||
|
|
||||||
### Build errors
|
|
||||||
```bash
|
|
||||||
# Check TypeScript version
|
|
||||||
npx tsc --version
|
|
||||||
|
|
||||||
# Clean and rebuild
|
|
||||||
rm -rf build/
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Runtime errors
|
|
||||||
Look for detailed error messages in the Claude Desktop logs:
|
|
||||||
```bash
|
|
||||||
# Monitor logs
|
|
||||||
tail -n 20 -f ~/Library/Logs/Claude/mcp*.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type errors
|
|
||||||
```bash
|
|
||||||
# Check types without building
|
|
||||||
npx tsc --noEmit
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next steps
|
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
|
||||||
<Card
|
|
||||||
title="Architecture overview"
|
|
||||||
icon="sitemap"
|
|
||||||
href="/docs/concepts/architecture"
|
|
||||||
>
|
|
||||||
Learn more about the MCP architecture
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
title="TypeScript SDK"
|
|
||||||
icon="square-js"
|
|
||||||
href="https://github.com/modelcontextprotocol/typescript-sdk"
|
|
||||||
>
|
|
||||||
Check out the TypeScript SDK on GitHub
|
|
||||||
</Card>
|
|
||||||
</CardGroup>
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Need help? Ask Claude! Since it has access to the MCP SDK documentation, it can help you debug issues and suggest improvements to your server.
|
|
||||||
</Note>
|
|
||||||
Loading…
Reference in a new issue