update the python impl
This commit is contained in:
parent
ddc20437e0
commit
f1649a310f
1 changed files with 374 additions and 238 deletions
|
|
@ -17,7 +17,6 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python --version # Should be 3.10 or higher
|
python --version # Should be 3.10 or higher
|
||||||
pip --version
|
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
|
|
@ -30,16 +29,15 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
|
|
||||||
<Step title="Create a new project">
|
<Step title="Create a new project">
|
||||||
```bash
|
```bash
|
||||||
mkdir weather-server
|
mkdir weather-service
|
||||||
cd weather-server
|
cd weather-service
|
||||||
uv init --package --app --name weather-server
|
uv init --package --app --name weather-service
|
||||||
source .venv/bin/activate # activate our virtual environment
|
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Install dependencies">
|
<Step title="Install dependencies">
|
||||||
```bash
|
```bash
|
||||||
uv add mcp
|
uv add mcp requests python-dotenv
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
|
|
@ -57,203 +55,174 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Create project structure">
|
<Step title="Create project structure">
|
||||||
```bash
|
```bash
|
||||||
touch weather_server.py
|
mkdir -p weather_service/src/weather_service
|
||||||
|
touch weather_service/src/weather_service/server.py
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Add the base code">
|
<Step title="Add the base imports and setup">
|
||||||
In `weather_server.py`
|
In `weather_service/src/weather_service/server.py`
|
||||||
```python
|
```python
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from typing import Any
|
from collections.abc import Sequence
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import asyncio
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from mcp.server import Server
|
from mcp.server import Server
|
||||||
from mcp.server.models import InitializationOptions
|
|
||||||
from mcp.types import (
|
from mcp.types import (
|
||||||
ServerCapabilities,
|
Resource,
|
||||||
ListResourcesResult,
|
Tool,
|
||||||
ReadResourceResult,
|
TextContent,
|
||||||
ListToolsResult,
|
ImageContent,
|
||||||
CallToolResult,
|
EmbeddedResource,
|
||||||
McpError,
|
LoggingLevel,
|
||||||
)
|
)
|
||||||
|
from pydantic import AnyUrl
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger("weather-server")
|
logger = logging.getLogger("weather-server")
|
||||||
|
|
||||||
|
# API configuration
|
||||||
API_KEY = os.getenv("OPENWEATHER_API_KEY")
|
API_KEY = os.getenv("OPENWEATHER_API_KEY")
|
||||||
if not API_KEY:
|
if not API_KEY:
|
||||||
raise ValueError("OPENWEATHER_API_KEY environment variable required")
|
raise ValueError("OPENWEATHER_API_KEY environment variable required")
|
||||||
|
|
||||||
API_BASE_URL = "http://api.openweathermap.org/data/2.5"
|
API_BASE_URL = "http://api.openweathermap.org/data/2.5"
|
||||||
DEFAULT_CITY = "San Francisco"
|
DEFAULT_CITY = "London"
|
||||||
CURRENT_WEATHER_ENDPOINT = "weather"
|
CURRENT_WEATHER_ENDPOINT = "weather"
|
||||||
FORECAST_ENDPOINT = "forecast"
|
FORECAST_ENDPOINT = "forecast"
|
||||||
|
|
||||||
class WeatherServer(Server):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__("example-weather-server")
|
|
||||||
|
|
||||||
# Set up HTTP session
|
|
||||||
self.http = requests.Session()
|
|
||||||
self.http.params = {
|
|
||||||
"appid": API_KEY,
|
|
||||||
"units": "metric"
|
|
||||||
}
|
|
||||||
|
|
||||||
@self.list_resources()
|
|
||||||
async def list_resources(self) -> ListResourcesResult:
|
|
||||||
# Implementation will come later
|
|
||||||
pass
|
|
||||||
|
|
||||||
@self.read_resource()
|
|
||||||
async def read_resource(self, uri: str) -> ReadResourceResult:
|
|
||||||
# Implementation will come later
|
|
||||||
pass
|
|
||||||
|
|
||||||
@self.list_tools()
|
|
||||||
async def list_tools(self) -> ListToolsResult:
|
|
||||||
# Implementation will come later
|
|
||||||
pass
|
|
||||||
|
|
||||||
@self.call_tool()
|
|
||||||
async def call_tool(self, name: str, args: Any) -> CallToolResult:
|
|
||||||
# Implementation will come later
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
|
async def mcp_server():
|
||||||
|
# our server implementation will go here
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Add resource handlers">
|
<Step title="Add weather fetching functionality">
|
||||||
Add these methods to the `WeatherServer` class:
|
Add this functionality:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@self.list_resources()
|
# Create reusable session
|
||||||
async def list_resources(self) -> ListResourcesResult:
|
http = requests.Session()
|
||||||
"""List available weather resources."""
|
http.params = {
|
||||||
return ListResourcesResult(
|
"appid": API_KEY,
|
||||||
resources=[
|
"units": "metric"
|
||||||
Resource(
|
}
|
||||||
uri=f"weather://{DEFAULT_CITY}/current",
|
|
||||||
name=f"Current weather in {DEFAULT_CITY}",
|
|
||||||
mimeType="application/json",
|
|
||||||
description="Real-time weather data"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.read_resource()
|
async def fetch_weather(city: str) -> dict[str, Any]:
|
||||||
async def read_resource(self, uri: AnyUrl) -> ReadResourceResult:
|
response = http.get(
|
||||||
"""Read current weather data for a city."""
|
f"{API_BASE_URL}/weather",
|
||||||
city = DEFAULT_CITY
|
params={"q": city}
|
||||||
if str(uri) != f"weather://{city}/current":
|
)
|
||||||
raise McpError(
|
response.raise_for_status()
|
||||||
ErrorData(
|
data = response.json()
|
||||||
code=INVALID_REQUEST,
|
|
||||||
message=f"Unknown resource: {uri}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
return {
|
||||||
response = self.http.get(
|
"temperature": data["main"]["temp"],
|
||||||
f"{API_BASE_URL}/{CURRENT_WEATHER_ENDPOINT}",
|
"conditions": data["weather"][0]["description"],
|
||||||
params={"q": city}
|
"humidity": data["main"]["humidity"],
|
||||||
)
|
"wind_speed": data["wind"]["speed"],
|
||||||
response.raise_for_status()
|
"timestamp": datetime.now().isoformat()
|
||||||
data = response.json()
|
}
|
||||||
|
|
||||||
weather_data = {
|
|
||||||
"temperature": data["main"]["temp"],
|
|
||||||
"conditions": data["weather"][0]["description"],
|
|
||||||
"humidity": data["main"]["humidity"],
|
|
||||||
"wind_speed": data["wind"]["speed"],
|
|
||||||
"timestamp": datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ReadResourceResult(
|
app = Server("weather-server")
|
||||||
contents=[
|
|
||||||
TextResourceContents(
|
|
||||||
uri=uri,
|
|
||||||
mimeType="application/json",
|
|
||||||
text=json.dumps(weather_data, indent=2)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
except requests.RequestException as e:
|
|
||||||
raise McpError(
|
|
||||||
ErrorData(
|
|
||||||
code=INTERNAL_ERROR,
|
|
||||||
message=f"Weather API error: {str(e)}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Add tool handlers">
|
<Step title="Implement resource handlers">
|
||||||
Add these methods to the `WeatherServer` class:
|
Add these resource-related handlers to our main function:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@self.list_tools()
|
app = Server("weather-server")
|
||||||
async def list_tools(self) -> ListToolsResult:
|
|
||||||
"""List available weather tools."""
|
|
||||||
return ListToolsResult(
|
|
||||||
tools=[
|
|
||||||
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"]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.call_tool()
|
@app.list_resources()
|
||||||
async def call_tool(self, name: str, arguments: Any) -> CallToolResult:
|
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 requests.RequestException 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."""
|
"""Handle tool calls for weather forecasts."""
|
||||||
if name != "get_forecast":
|
if name != "get_forecast":
|
||||||
raise McpError(
|
raise ValueError(f"Unknown tool: {name}")
|
||||||
ErrorData(
|
|
||||||
code=METHOD_NOT_FOUND,
|
|
||||||
message=f"Unknown tool: {name}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(arguments, dict) or "city" not in arguments:
|
if not isinstance(arguments, dict) or "city" not in arguments:
|
||||||
raise McpError(
|
raise ValueError("Invalid forecast arguments")
|
||||||
ErrorData(
|
|
||||||
code=INVALID_PARAMS,
|
|
||||||
message="Invalid forecast arguments"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
city = arguments["city"]
|
city = arguments["city"]
|
||||||
days = min(int(arguments.get("days", 3)), 5)
|
days = min(int(arguments.get("days", 3)), 5)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.session.get(
|
response = http.get(
|
||||||
f"{API_BASE_URL}/{FORECAST_ENDPOINT}",
|
f"{API_BASE_URL}/{FORECAST_ENDPOINT}",
|
||||||
params={
|
params={
|
||||||
"q": city,
|
"q": city,
|
||||||
|
|
@ -272,48 +241,50 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
"conditions": day_data["weather"][0]["description"]
|
"conditions": day_data["weather"][0]["description"]
|
||||||
})
|
})
|
||||||
|
|
||||||
return CallToolResult(
|
return [
|
||||||
content=[
|
TextContent(
|
||||||
TextContent(
|
type="text",
|
||||||
type="text",
|
text=json.dumps(forecasts, indent=2)
|
||||||
text=json.dumps(forecasts, indent=2)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
isError=False
|
|
||||||
)
|
|
||||||
except requests.RequestException as e:
|
|
||||||
raise McpError(
|
|
||||||
ErrorData(
|
|
||||||
code=INTERNAL_ERROR,
|
|
||||||
message=f"Weather API error: {str(e)}"
|
|
||||||
)
|
)
|
||||||
)
|
]
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.error(f"Weather API error: {str(e)}")
|
||||||
|
raise RuntimeError(f"Weather API error: {str(e)}")
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Add the main entry point">
|
<Step title="Add the main function">
|
||||||
Add this to the end of `weather_server.py`:
|
Add this to the end of `weather_service/src/weather_service/server.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def main():
|
async def main():
|
||||||
server = WeatherServer()
|
# Import here to avoid issues with event loops
|
||||||
|
from mcp.server.stdio import stdio_server
|
||||||
|
|
||||||
# Import here to avoid issues with event loops
|
async with stdio_server() as (read_stream, write_stream):
|
||||||
from mcp.server.stdio import stdio_server
|
await app.run(
|
||||||
|
read_stream,
|
||||||
async with stdio_server() as (read_stream, write_stream):
|
write_stream,
|
||||||
await server.run(
|
app.create_initialization_options()
|
||||||
read_stream,
|
)
|
||||||
write_stream,
|
|
||||||
server.create_initialization_options()
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import anyio
|
|
||||||
anyio.run(main)
|
|
||||||
```
|
```
|
||||||
</Step>
|
</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>
|
</Steps>
|
||||||
|
|
||||||
## Understanding the code
|
## Understanding the code
|
||||||
|
|
@ -330,7 +301,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
|
|
||||||
<Tab title="Resources">
|
<Tab title="Resources">
|
||||||
```python
|
```python
|
||||||
@self.list_resources()
|
@app.list_resources()
|
||||||
async def list_resources(self) -> ListResourcesResult:
|
async def list_resources(self) -> ListResourcesResult:
|
||||||
return ListResourcesResult(
|
return ListResourcesResult(
|
||||||
resources=[
|
resources=[
|
||||||
|
|
@ -373,6 +344,33 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
|
|
||||||
Tools let Claude take actions through your server with validated inputs.
|
Tools let Claude take actions through your server with validated inputs.
|
||||||
</Tab>
|
</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>
|
</Tabs>
|
||||||
|
|
||||||
## Best practices
|
## Best practices
|
||||||
|
|
@ -417,8 +415,36 @@ While this guide uses stdio transport, MCP supports additonal transport options:
|
||||||
### SSE (Server-Sent Events)
|
### SSE (Server-Sent Events)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from modelcontextprotocol import SSEServerTransport
|
from mcp.server.sse import SseServerTransport
|
||||||
transport = SSEServerTransport("/events", response)
|
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
|
## Troubleshooting
|
||||||
|
|
@ -431,18 +457,8 @@ python --version
|
||||||
|
|
||||||
# Reinstall dependencies
|
# Reinstall dependencies
|
||||||
uv sync --reinstall
|
uv sync --reinstall
|
||||||
|
|
||||||
# Ensure we are using the right virtual environment
|
|
||||||
source .venv/bin/activate
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Runtime errors
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enable debug logging
|
|
||||||
export MCP_LOG_LEVEL=DEBUG
|
|
||||||
python weather_server.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type checking
|
### Type checking
|
||||||
|
|
||||||
|
|
@ -451,7 +467,7 @@ python weather_server.py
|
||||||
uv add --dev pyright
|
uv add --dev pyright
|
||||||
|
|
||||||
# Run type checker
|
# Run type checker
|
||||||
pyright src
|
uv run pyright src
|
||||||
```
|
```
|
||||||
|
|
||||||
## Connect to Claude Desktop
|
## Connect to Claude Desktop
|
||||||
|
|
@ -515,30 +531,74 @@ pyright src
|
||||||
## Advanced features
|
## Advanced features
|
||||||
|
|
||||||
<Steps>
|
<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">
|
<Step title="Add caching">
|
||||||
```python
|
```python
|
||||||
from functools import lru_cache
|
# Cache settings
|
||||||
from datetime import datetime, timedelta
|
cache_timeout = timedelta(minutes=15)
|
||||||
|
last_cache_time = None
|
||||||
|
cached_weather = None
|
||||||
|
|
||||||
class WeatherServer:
|
async def fetch_weather(city: str) -> dict[str, Any]:
|
||||||
def __init__(self):
|
global cached_weather, last_cache_time
|
||||||
self._cache_timeout = timedelta(minutes=15)
|
|
||||||
self._last_cache_time = None
|
|
||||||
self._cached_weather = None
|
|
||||||
|
|
||||||
@lru_cache(maxsize=32)
|
now = datetime.now()
|
||||||
async def _fetch_weather(self, city: str) -> Dict[str, Any]:
|
if (cached_weather is None or
|
||||||
# Cache expires after 15 minutes
|
last_cache_time is None or
|
||||||
now = datetime.now()
|
now - last_cache_time > cache_timeout):
|
||||||
if (self._cached_weather is None or
|
|
||||||
self._last_cache_time is None or
|
|
||||||
now - self._last_cache_time > self._cache_timeout):
|
|
||||||
|
|
||||||
response = self.session.get(...)
|
response = http.get(
|
||||||
self._cached_weather = response.json()
|
f"{API_BASE_URL}/{CURRENT_WEATHER_ENDPOINT}",
|
||||||
self._last_cache_time = now
|
params={"q": city}
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
return self._cached_weather
|
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>
|
||||||
|
|
||||||
|
|
@ -615,46 +675,122 @@ pyright src
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Create test file">
|
<Step title="Create test file">
|
||||||
Create `src/weather_service/tests/test_weather_server.py`:
|
Create `tests/weather_test.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import Mock, patch
|
import os
|
||||||
from weather_service.weather_server import WeatherServer
|
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
|
@pytest.fixture
|
||||||
async def server():
|
def mock_weather_response():
|
||||||
server = WeatherServer()
|
return {
|
||||||
yield server
|
"main": {
|
||||||
|
"temp": 20.5,
|
||||||
|
"humidity": 65
|
||||||
|
},
|
||||||
|
"weather": [
|
||||||
|
{"description": "scattered clouds"}
|
||||||
|
],
|
||||||
|
"wind": {
|
||||||
|
"speed": 3.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.fixture
|
||||||
async def test_list_resources(server):
|
def mock_forecast_response():
|
||||||
result = await server.list_resources()
|
return {
|
||||||
assert len(result.resources) > 0
|
"list": [
|
||||||
assert result.resources[0].mimeType == "application/json"
|
{
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_forecast():
|
|
||||||
with patch("requests.Session") as mock_session:
|
|
||||||
mock_session.return_value.get.return_value.json.return_value = {
|
|
||||||
"list": [{
|
|
||||||
"dt_txt": "2024-01-01 12:00:00",
|
"dt_txt": "2024-01-01 12:00:00",
|
||||||
"main": {"temp": 20},
|
"main": {"temp": 18.5},
|
||||||
"weather": [{"description": "sunny"}]
|
"weather": [{"description": "sunny"}]
|
||||||
}]
|
},
|
||||||
|
{
|
||||||
|
"dt_txt": "2024-01-02 12:00:00",
|
||||||
|
"main": {"temp": 17.2},
|
||||||
|
"weather": [{"description": "cloudy"}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
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.asyncio
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
server = WeatherServer()
|
uri = AnyUrl("weather://London/current")
|
||||||
result = await server.call_tool("get_forecast", {"city": "Tokyo", "days": 1})
|
result = await read_resource(uri)
|
||||||
assert len(result.content) == 1
|
|
||||||
assert "temperature" in result.content[0].text
|
assert isinstance(result, str)
|
||||||
assert "20" in result.content[0].text
|
assert "temperature" in result
|
||||||
|
assert "clear sky" in result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_call_tool(mock_forecast_response):
|
||||||
|
with patch('weather_service.server.http.get') as mock_get:
|
||||||
|
mock_get.return_value.json.return_value = mock_forecast_response
|
||||||
|
mock_get.return_value.raise_for_status = Mock()
|
||||||
|
|
||||||
|
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.asyncio
|
||||||
|
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.asyncio
|
||||||
|
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>
|
||||||
<Step title="Run tests">
|
<Step title="Run tests">
|
||||||
```bash
|
```bash
|
||||||
uv add --dev pytest pytest-asyncio
|
uv add --dev pytest pytest-asyncio
|
||||||
uv run pytest src/weather_service/
|
uv run pytest
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue