Almost done with Python guide

This commit is contained in:
Zack Lee 2024-11-21 13:00:50 -05:00
commit 2ea1edebae
No known key found for this signature in database

View file

@ -37,6 +37,8 @@ Let's build your first MCP server in Python! We'll create a weather server that
```bash
uvx create-mcp-server --path weather_service
cd weather_service
# Installs base dependencies
uv sync --dev --all-extras
```
</Step>
@ -55,37 +57,102 @@ Let's build your first MCP server in Python! We'll create a weather server that
</Step>
</Steps>
## Create your server
## Understanding the starter code
The starter code establishes a basic MCP server structure that we'll build upon to create our weather service. Let's break down its key components:
### Server class definition
```python
class McpServer(Server):
def __init__(self):
super().__init__("weather_service")
self.notes: dict[str, str] = {}
```
This is our main server class that inherits from the base `Server` class. Currently, it initializes with a simple notes dictionary that we'll replace with weather-related functionality.
### Three core primitives
The server implements three fundamental MCP primitives, two of which we'll use for our weather service:
1. **Resources (`@self.list_resources` and `@self.read_resource`)**
- Will be modified to expose a single weather endpoint for DEFAULT_CITY
- `list_resources`: Returns current weather resource for DEFAULT_CITY
- `read_resource`: Fetches and returns current weather data for DEFAULT_CITY
- Uses custom URI schemes for data access
2. **Tools (`@self.list_tools` and `@self.call_tool`)**
- Will implement a single "get_forecast" tool
- Accepts a city name as input
- Returns weather forecast data for the specified city
- Uses JSON Schema for input validation
### Server communication
```python
async def main():
server = McpServer()
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(...)
```
This section handles server initialization and communication setup using stdin/stdout streams.
## Steps to implement our weather server
To transform this starter code into our weather server, we'll:
1. **Set up OpenWeather integration**
- Add OpenWeather API key configuration
- Create helper functions for API communication
- Implement weather data parsing
2. **Implement weather resources**
- Replace note storage with weather data handling
- Create weather-specific URI scheme
- Add current weather fetching for DEFAULT_CITY
3. **Create forecast tool**
- Remove note-related tools
- Add "get_forecast" tool implementation
- Implement city-based forecast fetching
- Add response formatting
4. **Add error handling**
- Implement API error management
- Add input validation for city names
- Create user-friendly error messages
Let's dive in!
## Implementing the weather server
Let's start by modifiying our existing `server.py` to transform it from a notes server into a weather service. We'll go through each section of the code and make the necessary changes.
<Steps>
<Step title="Add the base imports and setup">
In `weather_service/src/weather_service/server.py`
<Step title="Updating imports and configuration">
First, let's add the imports we need at the top of the file:
```python
# Keep existing imports
import asyncio
from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
from pydantic import AnyUrl
import mcp.server.stdio
# Add new imports
import os
import json
import logging
from datetime import datetime, timedelta
from collections.abc import Sequence
from functools import lru_cache
from datetime import datetime
import requests
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
# Add configuration
load_dotenv()
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("weather-server")
@ -98,25 +165,40 @@ Let's build your first MCP server in Python! We'll create a weather server that
DEFAULT_CITY = "London"
CURRENT_WEATHER_ENDPOINT = "weather"
FORECAST_ENDPOINT = "forecast"
async def mcp_server():
# our server implementation will go here
```
</Step>
<Step title="Add weather fetching functionality">
Add this functionality:
<Step title="Update the class initialization">
First, let's modify the class docstring and initialization to remove the notes storage and add our HTTP session:
```python
# Create reusable session
http = requests.Session()
http.params = {
class McpServer(Server):
"""
Weather service MCP server implementing two core primitives:
1. Resources - Current weather data accessed via custom URIs
2. Tools - Ability to fetch weather forecasts for any city
"""
def __init__(self):
super().__init__("weather_service")
# Create reusable session for API calls
self.http = requests.Session()
self.http.params = {
"appid": API_KEY,
"units": "metric"
}
```
</Step>
async def fetch_weather(city: str) -> dict[str, Any]:
response = http.get(
<Step title="Add weather helper method">
Next, let's add a helper method to fetch weather data that both our resource and tool handlers will use:
```python
async def fetch_weather(self, city: str) -> dict:
"""Helper method to fetch current weather data."""
response = self.http.get(
f"{API_BASE_URL}/weather",
params={"q": city}
)
@ -130,24 +212,20 @@ Let's build your first MCP server in Python! We'll create a weather server that
"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:
<Step title="Update resource handlers">
Now, let's replace the note-related resource handlers with weather ones:
```python
app = Server("weather-server")
@app.list_resources()
async def list_resources() -> list[Resource]:
# RESOURCE HANDLERS
@self.list_resources()
async def handle_list_resources(self) -> list[types.Resource]:
"""List available weather resources."""
uri = AnyUrl(f"weather://{DEFAULT_CITY}/current")
return [
Resource(
types.Resource(
uri=uri,
name=f"Current weather in {DEFAULT_CITY}",
mimeType="application/json",
@ -155,37 +233,36 @@ Let's build your first MCP server in Python! We'll create a weather server that
)
]
@app.read_resource()
async def read_resource(uri: AnyUrl) -> str:
@self.read_resource()
async def handle_read_resource(self, 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)
weather_data = await self.fetch_weather(city)
return json.dumps(weather_data, indent=2)
except requests.RequestException as e:
raise RuntimeError(f"Weather API error: {str(e)}")
```
</Step>
<Step title="Implement tool handlers">
Add these tool-related handlers:
<Step title="Remove prompt handlers">
Remove the entire prompt section from the starter code - we won't be using prompts in our weather service.
</Step>
<Step title="Update tool handlers">
Finally, let's replace the note-adding tool with our weather forecast tool:
```python
app = Server("weather-server")
# Resource implementation ...
@app.list_tools()
async def list_tools() -> list[Tool]:
# TOOL HANDLERS
@self.list_tools()
async def handle_list_tools(self) -> list[types.Tool]:
"""List available weather tools."""
return [
Tool(
types.Tool(
name="get_forecast",
description="Get weather forecast for a city",
inputSchema={
@ -207,8 +284,10 @@ Let's build your first MCP server in Python! We'll create a weather server that
)
]
@app.call_tool()
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
@self.call_tool()
async def handle_call_tool(
self, name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""Handle tool calls for weather forecasts."""
if name != "get_forecast":
raise ValueError(f"Unknown tool: {name}")
@ -220,7 +299,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
days = min(int(arguments.get("days", 3)), 5)
try:
response = http.get(
response = self.http.get(
f"{API_BASE_URL}/{FORECAST_ENDPOINT}",
params={
"q": city,
@ -240,7 +319,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
})
return [
TextContent(
types.TextContent(
type="text",
text=json.dumps(forecasts, indent=2)
)
@ -250,226 +329,16 @@ Let's build your first MCP server in Python! We'll create a weather server that
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>
## 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:
response = self.http.get(...)
response.raise_for_status()
except requests.RequestException 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 additonal 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)
```
## 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
```
## Connect to Claude Desktop
Now that our server is implemented, let's make sure Claude desktop can talk to it properly.
<Note>
Note that this step is unnecessary if you've opted to install the server into Claude.app with the MCP project creator script.
</Note>
<Steps>
<Step title="Update Claude config">
Add to `claude_desktop_config.json`: