Improve Python First Server
This is a first pass as improving the Python First Server documentation. There is still much left and I haven't tested it yet.
This commit is contained in:
parent
2755937ddc
commit
ddc20437e0
1 changed files with 295 additions and 368 deletions
|
|
@ -33,6 +33,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
mkdir weather-server
|
mkdir weather-server
|
||||||
cd weather-server
|
cd weather-server
|
||||||
uv init --package --app --name weather-server
|
uv init --package --app --name weather-server
|
||||||
|
source .venv/bin/activate # activate our virtual environment
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
|
|
@ -61,113 +62,110 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Add the base code">
|
<Step title="Add the base code">
|
||||||
In `weather_server.py`:
|
In `weather_server.py`
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
```python
|
import requests
|
||||||
import os
|
from dotenv import load_dotenv
|
||||||
import json
|
from mcp.server import Server
|
||||||
from datetime import datetime
|
from mcp.server.models import InitializationOptions
|
||||||
from typing import Any, Dict, List, Optional
|
from mcp.types import (
|
||||||
|
ServerCapabilities,
|
||||||
|
ListResourcesResult,
|
||||||
|
ReadResourceResult,
|
||||||
|
ListToolsResult,
|
||||||
|
CallToolResult,
|
||||||
|
McpError,
|
||||||
|
)
|
||||||
|
|
||||||
import requests
|
load_dotenv()
|
||||||
from dotenv import load_dotenv
|
|
||||||
from modelcontextprotocol import Server, StdioServerTransport
|
|
||||||
from modelcontextprotocol.types import (
|
|
||||||
ListResourcesRequest,
|
|
||||||
ReadResourceRequest,
|
|
||||||
ListToolsRequest,
|
|
||||||
CallToolRequest,
|
|
||||||
ErrorCode,
|
|
||||||
McpError
|
|
||||||
)
|
|
||||||
|
|
||||||
load_dotenv()
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger("weather-server")
|
||||||
|
|
||||||
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 is required")
|
raise ValueError("OPENWEATHER_API_KEY environment variable required")
|
||||||
|
|
||||||
API_CONFIG = {
|
API_BASE_URL = "http://api.openweathermap.org/data/2.5"
|
||||||
"BASE_URL": "http://api.openweathermap.org/data/2.5",
|
DEFAULT_CITY = "San Francisco"
|
||||||
"DEFAULT_CITY": "San Francisco",
|
CURRENT_WEATHER_ENDPOINT = "weather"
|
||||||
"ENDPOINTS": {
|
FORECAST_ENDPOINT = "forecast"
|
||||||
"CURRENT": "weather",
|
|
||||||
"FORECAST": "forecast"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WeatherServer:
|
class WeatherServer(Server):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.server = Server(
|
super().__init__("example-weather-server")
|
||||||
name="example-weather-server",
|
|
||||||
version="0.1.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.session = requests.Session()
|
|
||||||
self.session.params = {
|
|
||||||
"appid": API_KEY,
|
|
||||||
"units": "metric"
|
|
||||||
}
|
|
||||||
|
|
||||||
self.setup_handlers()
|
# Set up HTTP session
|
||||||
self.setup_error_handling()
|
self.http = requests.Session()
|
||||||
|
self.http.params = {
|
||||||
|
"appid": API_KEY,
|
||||||
|
"units": "metric"
|
||||||
|
}
|
||||||
|
|
||||||
def setup_error_handling(self) -> None:
|
@self.list_resources()
|
||||||
def on_error(error: Exception) -> None:
|
async def list_resources(self) -> ListResourcesResult:
|
||||||
print("[MCP Error]", error, flush=True)
|
# Implementation will come later
|
||||||
|
pass
|
||||||
|
|
||||||
self.server.onerror = on_error
|
@self.read_resource()
|
||||||
|
async def read_resource(self, uri: str) -> ReadResourceResult:
|
||||||
|
# Implementation will come later
|
||||||
|
pass
|
||||||
|
|
||||||
def setup_handlers(self) -> None:
|
@self.list_tools()
|
||||||
self.server.set_request_handler(
|
async def list_tools(self) -> ListToolsResult:
|
||||||
ListResourcesRequest, self.handle_list_resources)
|
# Implementation will come later
|
||||||
self.server.set_request_handler(
|
pass
|
||||||
ReadResourceRequest, self.handle_read_resource)
|
|
||||||
self.server.set_request_handler(
|
|
||||||
ListToolsRequest, self.handle_list_tools)
|
|
||||||
self.server.set_request_handler(
|
|
||||||
CallToolRequest, self.handle_call_tool)
|
|
||||||
|
|
||||||
async def run(self) -> None:
|
@self.call_tool()
|
||||||
transport = StdioServerTransport()
|
async def call_tool(self, name: str, args: Any) -> CallToolResult:
|
||||||
await self.server.connect(transport)
|
# Implementation will come later
|
||||||
print("Weather MCP server running on stdio", flush=True)
|
pass
|
||||||
|
|
||||||
# Handler implementations will continue in next sections
|
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Add resource handlers">
|
<Step title="Add resource handlers">
|
||||||
Add these methods to the `WeatherServer` class:
|
Add these methods to the `WeatherServer` class:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def handle_list_resources(
|
@self.list_resources()
|
||||||
self,
|
async def list_resources(self) -> ListResourcesResult:
|
||||||
request: ListResourcesRequest
|
"""List available weather resources."""
|
||||||
) -> Dict[str, Any]:
|
return ListResourcesResult(
|
||||||
return {
|
resources=[
|
||||||
"resources": [{
|
Resource(
|
||||||
"uri": f"weather://{API_CONFIG['DEFAULT_CITY']}/current",
|
uri=f"weather://{DEFAULT_CITY}/current",
|
||||||
"name": f"Current weather in {API_CONFIG['DEFAULT_CITY']}",
|
name=f"Current weather in {DEFAULT_CITY}",
|
||||||
"mimeType": "application/json",
|
mimeType="application/json",
|
||||||
"description": "Real-time weather data including temperature, conditions, humidity, and wind speed"
|
description="Real-time weather data"
|
||||||
}]
|
)
|
||||||
}
|
]
|
||||||
|
)
|
||||||
|
|
||||||
async def handle_read_resource(
|
@self.read_resource()
|
||||||
self,
|
async def read_resource(self, uri: AnyUrl) -> ReadResourceResult:
|
||||||
request: ReadResourceRequest
|
"""Read current weather data for a city."""
|
||||||
) -> Dict[str, Any]:
|
city = DEFAULT_CITY
|
||||||
city = API_CONFIG["DEFAULT_CITY"]
|
if str(uri) != f"weather://{city}/current":
|
||||||
if request.params.uri != f"weather://{city}/current":
|
|
||||||
raise McpError(
|
raise McpError(
|
||||||
ErrorCode.INVALID_REQUEST,
|
ErrorData(
|
||||||
f"Unknown resource: {request.params.uri}"
|
code=INVALID_REQUEST,
|
||||||
|
message=f"Unknown resource: {uri}"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.session.get(
|
response = self.http.get(
|
||||||
f"{API_CONFIG['BASE_URL']}/{API_CONFIG['ENDPOINTS']['CURRENT']}",
|
f"{API_BASE_URL}/{CURRENT_WEATHER_ENDPOINT}",
|
||||||
params={"q": city}
|
params={"q": city}
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
@ -181,17 +179,21 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
"timestamp": datetime.now().isoformat()
|
"timestamp": datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return ReadResourceResult(
|
||||||
"contents": [{
|
contents=[
|
||||||
"uri": request.params.uri,
|
TextResourceContents(
|
||||||
"mimeType": "application/json",
|
uri=uri,
|
||||||
"text": json.dumps(weather_data, indent=2)
|
mimeType="application/json",
|
||||||
}]
|
text=json.dumps(weather_data, indent=2)
|
||||||
}
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
raise McpError(
|
raise McpError(
|
||||||
ErrorCode.INTERNAL_ERROR,
|
ErrorData(
|
||||||
f"Weather API error: {str(e)}"
|
code=INTERNAL_ERROR,
|
||||||
|
message=f"Weather API error: {str(e)}"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
@ -200,56 +202,59 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
Add these methods to the `WeatherServer` class:
|
Add these methods to the `WeatherServer` class:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def handle_list_tools(
|
@self.list_tools()
|
||||||
self,
|
async def list_tools(self) -> ListToolsResult:
|
||||||
request: ListToolsRequest
|
"""List available weather tools."""
|
||||||
) -> Dict[str, Any]:
|
return ListToolsResult(
|
||||||
return {
|
tools=[
|
||||||
"tools": [{
|
Tool(
|
||||||
"name": "get_forecast",
|
name="get_forecast",
|
||||||
"description": "Get weather forecast for a city",
|
description="Get weather forecast for a city",
|
||||||
"inputSchema": {
|
inputSchema={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"city": {
|
"city": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "City name"
|
"description": "City name"
|
||||||
|
},
|
||||||
|
"days": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Number of days (1-5)",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 5
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"days": {
|
"required": ["city"]
|
||||||
"type": "number",
|
}
|
||||||
"description": "Number of days (1-5)",
|
)
|
||||||
"minimum": 1,
|
]
|
||||||
"maximum": 5
|
)
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["city"]
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
async def handle_call_tool(
|
@self.call_tool()
|
||||||
self,
|
async def call_tool(self, name: str, arguments: Any) -> CallToolResult:
|
||||||
request: CallToolRequest
|
"""Handle tool calls for weather forecasts."""
|
||||||
) -> Dict[str, Any]:
|
if name != "get_forecast":
|
||||||
if request.params.name != "get_forecast":
|
|
||||||
raise McpError(
|
raise McpError(
|
||||||
ErrorCode.METHOD_NOT_FOUND,
|
ErrorData(
|
||||||
f"Unknown tool: {request.params.name}"
|
code=METHOD_NOT_FOUND,
|
||||||
|
message=f"Unknown tool: {name}"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
args = request.params.arguments
|
if not isinstance(arguments, dict) or "city" not in arguments:
|
||||||
if not isinstance(args, dict) or "city" not in args:
|
|
||||||
raise McpError(
|
raise McpError(
|
||||||
ErrorCode.INVALID_PARAMS,
|
ErrorData(
|
||||||
"Invalid forecast arguments"
|
code=INVALID_PARAMS,
|
||||||
|
message="Invalid forecast arguments"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
city = args["city"]
|
city = arguments["city"]
|
||||||
days = min(int(args.get("days", 3)), 5)
|
days = min(int(arguments.get("days", 3)), 5)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.session.get(
|
response = self.session.get(
|
||||||
f"{API_CONFIG['BASE_URL']}/{API_CONFIG['ENDPOINTS']['FORECAST']}",
|
f"{API_BASE_URL}/{FORECAST_ENDPOINT}",
|
||||||
params={
|
params={
|
||||||
"q": city,
|
"q": city,
|
||||||
"cnt": days * 8 # API returns 3-hour intervals
|
"cnt": days * 8 # API returns 3-hour intervals
|
||||||
|
|
@ -267,11 +272,21 @@ 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 {"toolResult": forecasts}
|
return CallToolResult(
|
||||||
|
content=[
|
||||||
|
TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(forecasts, indent=2)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
isError=False
|
||||||
|
)
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
raise McpError(
|
raise McpError(
|
||||||
ErrorCode.INTERNAL_ERROR,
|
ErrorData(
|
||||||
f"Weather API error: {str(e)}"
|
code=INTERNAL_ERROR,
|
||||||
|
message=f"Weather API error: {str(e)}"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
@ -280,52 +295,25 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
Add this to the end of `weather_server.py`:
|
Add this to the end of `weather_server.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
if __name__ == "__main__":
|
async def main():
|
||||||
import asyncio
|
server = WeatherServer()
|
||||||
import signal
|
|
||||||
|
|
||||||
async def main():
|
# Import here to avoid issues with event loops
|
||||||
server = WeatherServer()
|
from mcp.server.stdio import stdio_server
|
||||||
|
|
||||||
def signal_handler(sig, frame):
|
|
||||||
print("Shutting down...", flush=True)
|
|
||||||
asyncio.create_task(server.server.close())
|
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
|
||||||
await server.run()
|
|
||||||
|
|
||||||
asyncio.run(main())
|
async with stdio_server() as (read_stream, write_stream):
|
||||||
|
await server.run(
|
||||||
|
read_stream,
|
||||||
|
write_stream,
|
||||||
|
server.create_initialization_options()
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import anyio
|
||||||
|
anyio.run(main)
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Make the script executable">
|
|
||||||
```bash
|
|
||||||
chmod +x weather_server.py
|
|
||||||
pip install -e .
|
|
||||||
```
|
|
||||||
|
|
||||||
Create `setup.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="weather-server",
|
|
||||||
version="0.1.0",
|
|
||||||
py_modules=["weather_server"],
|
|
||||||
entry_points={
|
|
||||||
"console_scripts": [
|
|
||||||
"weather-server=weather_server:main",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
install_requires=[
|
|
||||||
"modelcontextprotocol",
|
|
||||||
"requests",
|
|
||||||
"python-dotenv"
|
|
||||||
],
|
|
||||||
)
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
## Understanding the code
|
## Understanding the code
|
||||||
|
|
@ -333,10 +321,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Type Hints">
|
<Tab title="Type Hints">
|
||||||
```python
|
```python
|
||||||
async def handle_read_resource(
|
async def read_resource(self, uri: str) -> ReadResourceResult:
|
||||||
self,
|
|
||||||
request: ReadResourceRequest
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -345,17 +330,18 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
|
|
||||||
<Tab title="Resources">
|
<Tab title="Resources">
|
||||||
```python
|
```python
|
||||||
async def handle_list_resources(
|
@self.list_resources()
|
||||||
self,
|
async def list_resources(self) -> ListResourcesResult:
|
||||||
request: ListResourcesRequest
|
return ListResourcesResult(
|
||||||
) -> Dict[str, Any]:
|
resources=[
|
||||||
return {
|
Resource(
|
||||||
"resources": [{
|
uri=f"weather://{DEFAULT_CITY}/current",
|
||||||
"uri": f"weather://{DEFAULT_CITY}/current",
|
name=f"Current weather in {DEFAULT_CITY}",
|
||||||
"name": f"Current weather in {DEFAULT_CITY}",
|
mimeType="application/json",
|
||||||
"mimeType": "application/json"
|
description="Real-time weather data"
|
||||||
}]
|
)
|
||||||
}
|
]
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Resources provide data that Claude can access as context.
|
Resources provide data that Claude can access as context.
|
||||||
|
|
@ -363,17 +349,26 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
|
|
||||||
<Tab title="Tools">
|
<Tab title="Tools">
|
||||||
```python
|
```python
|
||||||
{
|
Tool(
|
||||||
"name": "get_forecast",
|
name="get_forecast",
|
||||||
"description": "Get weather forecast for a city",
|
description="Get weather forecast for a city",
|
||||||
"inputSchema": {
|
inputSchema={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"city": { "type": "string" },
|
"city": {
|
||||||
"days": { "type": "number" }
|
"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.
|
Tools let Claude take actions through your server with validated inputs.
|
||||||
|
|
@ -386,7 +381,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
<Card title="Error Handling" icon="shield">
|
<Card title="Error Handling" icon="shield">
|
||||||
```python
|
```python
|
||||||
try:
|
try:
|
||||||
response = self.session.get(...)
|
response = self.http.get(...)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
raise McpError(
|
raise McpError(
|
||||||
|
|
@ -413,29 +408,11 @@ Let's build your first MCP server in Python! We'll create a weather server that
|
||||||
```
|
```
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title="Constants" icon="box">
|
|
||||||
```python
|
|
||||||
API_CONFIG = {
|
|
||||||
"BASE_URL": "...",
|
|
||||||
"ENDPOINTS": {
|
|
||||||
"CURRENT": "weather",
|
|
||||||
"FORECAST": "forecast"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</Card>
|
|
||||||
</CardGroup>
|
</CardGroup>
|
||||||
|
|
||||||
## Available transports
|
## Available transports
|
||||||
|
|
||||||
While this guide uses stdio transport, MCP supports multiple transport options:
|
While this guide uses stdio transport, MCP supports additonal transport options:
|
||||||
|
|
||||||
### WebSocket
|
|
||||||
|
|
||||||
```python
|
|
||||||
from modelcontextprotocol import WebSocketServerTransport
|
|
||||||
transport = WebSocketServerTransport()
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSE (Server-Sent Events)
|
### SSE (Server-Sent Events)
|
||||||
|
|
||||||
|
|
@ -453,8 +430,10 @@ transport = SSEServerTransport("/events", response)
|
||||||
python --version
|
python --version
|
||||||
|
|
||||||
# Reinstall dependencies
|
# Reinstall dependencies
|
||||||
pip uninstall -r requirements.txt
|
uv sync --reinstall
|
||||||
pip install -r requirements.txt
|
|
||||||
|
# Ensure we are using the right virtual environment
|
||||||
|
source .venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
### Runtime errors
|
### Runtime errors
|
||||||
|
|
@ -469,10 +448,10 @@ python weather_server.py
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install mypy
|
# Install mypy
|
||||||
pip install mypy
|
uv add --dev pyright
|
||||||
|
|
||||||
# Run type checker
|
# Run type checker
|
||||||
mypy weather_server.py
|
pyright src
|
||||||
```
|
```
|
||||||
|
|
||||||
## Connect to Claude Desktop
|
## Connect to Claude Desktop
|
||||||
|
|
@ -485,7 +464,8 @@ mypy weather_server.py
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"weather": {
|
"weather": {
|
||||||
"command": "weather-server",
|
"command": "uv",
|
||||||
|
"args": "--directory path/to/your/project run weather-service",
|
||||||
"env": {
|
"env": {
|
||||||
"OPENWEATHER_API_KEY": "your-api-key",
|
"OPENWEATHER_API_KEY": "your-api-key",
|
||||||
}
|
}
|
||||||
|
|
@ -550,45 +530,39 @@ mypy weather_server.py
|
||||||
async def _fetch_weather(self, city: str) -> Dict[str, Any]:
|
async def _fetch_weather(self, city: str) -> Dict[str, Any]:
|
||||||
# Cache expires after 15 minutes
|
# Cache expires after 15 minutes
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
if (self._cached_weather is None or
|
if (self._cached_weather is None or
|
||||||
self._last_cache_time is None or
|
self._last_cache_time is None or
|
||||||
now - self._last_cache_time > self._cache_timeout):
|
now - self._last_cache_time > self._cache_timeout):
|
||||||
|
|
||||||
response = self.session.get(...)
|
response = self.session.get(...)
|
||||||
self._cached_weather = response.json()
|
self._cached_weather = response.json()
|
||||||
self._last_cache_time = now
|
self._last_cache_time = now
|
||||||
|
|
||||||
return self._cached_weather
|
return self._cached_weather
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Add progress notifications">
|
<Step title="Add progress notifications">
|
||||||
```python
|
```python
|
||||||
async def handle_call_tool(
|
@self.call_tool()
|
||||||
self,
|
async def call_tool(self, name: str, arguments: Any) -> CallToolResult:
|
||||||
request: CallToolRequest
|
if progress_token := self.request_context.meta.progressToken:
|
||||||
) -> Dict[str, Any]:
|
|
||||||
if progress_token := request.params._meta.get("progressToken"):
|
|
||||||
# Send progress notifications
|
# Send progress notifications
|
||||||
await self.server.notification({
|
await self.request_context.session.send_progress_notification(
|
||||||
"method": "notifications/progress",
|
progress_token=progress_token,
|
||||||
"params": {
|
progress=1,
|
||||||
"progress": 1,
|
total=2
|
||||||
"total": 2,
|
)
|
||||||
"progressToken": progress_token
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Fetch data...
|
# Fetch data...
|
||||||
|
|
||||||
await self.server.notification({
|
await self.request_context.session.send_progress_notification(
|
||||||
"method": "notifications/progress",
|
progress_token=progress_token,
|
||||||
"params": {
|
progress=2,
|
||||||
"progress": 2,
|
total=2
|
||||||
"total": 2,
|
)
|
||||||
"progressToken": progress_token
|
|
||||||
}
|
# Rest of the method implementation...
|
||||||
})
|
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
|
|
@ -596,50 +570,43 @@ mypy weather_server.py
|
||||||
```python
|
```python
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
class WeatherServer:
|
class WeatherServer(Server):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__("example-weather-server")
|
||||||
self.logger = logging.getLogger("weather-server")
|
self.logger = logging.getLogger("weather-server")
|
||||||
self.logger.setLevel(logging.INFO)
|
self.logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
# Set up logging handler
|
|
||||||
self.server.set_request_handler(
|
|
||||||
SetLevelRequest,
|
|
||||||
self.handle_set_log_level
|
|
||||||
)
|
|
||||||
|
|
||||||
async def handle_set_log_level(
|
@self.set_logging_level()
|
||||||
self,
|
async def set_logging_level(self, level: LoggingLevel) -> EmptyResult:
|
||||||
request: SetLevelRequest
|
self.logger.setLevel(level.upper())
|
||||||
) -> Dict[str, Any]:
|
await self.request_context.session.send_log_message(
|
||||||
level = request.params.level.upper()
|
level="info",
|
||||||
self.logger.setLevel(getattr(logging, level))
|
data=f"Log level set to {level}",
|
||||||
|
logger="weather-server"
|
||||||
await self.server.notification({
|
)
|
||||||
"method": "notifications/message",
|
return EmptyResult()
|
||||||
"params": {
|
|
||||||
"level": "info",
|
# Use self.logger for logging throughout the class methods
|
||||||
"logger": "weather-server",
|
# For example:
|
||||||
"data": f"Log level set to {level}"
|
# self.logger.info("Weather data fetched successfully")
|
||||||
}
|
# self.logger.error(f"Error fetching weather data: {str(e)}")
|
||||||
})
|
|
||||||
return {}
|
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Add resource templates">
|
<Step title="Add resource templates">
|
||||||
```python
|
```python
|
||||||
async def handle_list_resources(
|
@self.list_resources()
|
||||||
self,
|
async def list_resources(self) -> ListResourcesResult:
|
||||||
request: ListResourcesRequest
|
return ListResourcesResult(
|
||||||
) -> Dict[str, Any]:
|
resources=[...],
|
||||||
return {
|
resourceTemplates=[
|
||||||
"resources": [...],
|
ResourceTemplate(
|
||||||
"resourceTemplates": [{
|
uriTemplate="weather://{city}/current",
|
||||||
"uriTemplate": "weather://{city}/current",
|
name="Current weather for any city",
|
||||||
"name": "Current weather for any city",
|
mimeType="application/json"
|
||||||
"mimeType": "application/json"
|
)
|
||||||
}]
|
]
|
||||||
}
|
)
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
@ -648,90 +615,50 @@ mypy weather_server.py
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Create test file">
|
<Step title="Create test file">
|
||||||
Create `test_weather_server.py`:
|
Create `src/weather_service/tests/test_weather_server.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
from weather_server import WeatherServer
|
from weather_service.weather_server import WeatherServer
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def server():
|
async def server():
|
||||||
server = WeatherServer()
|
server = WeatherServer()
|
||||||
yield server
|
yield server
|
||||||
await server.server.close()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_list_resources(server):
|
async def test_list_resources(server):
|
||||||
request = Mock()
|
result = await server.list_resources()
|
||||||
result = await server.handle_list_resources(request)
|
assert len(result.resources) > 0
|
||||||
assert len(result["resources"]) > 0
|
assert result.resources[0].mimeType == "application/json"
|
||||||
assert result["resources"][0]["mimeType"] == "application/json"
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_forecast():
|
async def test_get_forecast():
|
||||||
with patch("requests.Session") as mock_session:
|
with patch("requests.Session") as mock_session:
|
||||||
mock_session.return_value.get.return_value.json.return_value = {
|
mock_session.return_value.get.return_value.json.return_value = {
|
||||||
"list": [{
|
"list": [{
|
||||||
"dt_txt": "2024-01-01 12:00:00",
|
"dt_txt": "2024-01-01 12:00:00",
|
||||||
"main": {"temp": 20},
|
"main": {"temp": 20},
|
||||||
"weather": [{"description": "sunny"}]
|
"weather": [{"description": "sunny"}]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
server = WeatherServer()
|
server = WeatherServer()
|
||||||
request = Mock()
|
result = await server.call_tool("get_forecast", {"city": "Tokyo", "days": 1})
|
||||||
request.params.name = "get_forecast"
|
assert len(result.content) == 1
|
||||||
request.params.arguments = {"city": "Tokyo", "days": 1}
|
assert "temperature" in result.content[0].text
|
||||||
|
assert "20" in result.content[0].text
|
||||||
result = await server.handle_call_tool(request)
|
```
|
||||||
assert len(result["toolResult"]) == 1
|
|
||||||
assert result["toolResult"][0]["temperature"] == 20
|
|
||||||
```
|
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Run tests">
|
<Step title="Run tests">
|
||||||
```bash
|
```bash
|
||||||
pip install pytest pytest-asyncio
|
uv add --dev pytest pytest-asyncio
|
||||||
pytest test_weather_server.py -v
|
uv run pytest src/weather_service/
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
<CardGroup cols={1}>
|
|
||||||
<Card title="Docker" icon="docker">
|
|
||||||
```dockerfile
|
|
||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install -r requirements.txt
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
CMD ["python", "weather_server.py"]
|
|
||||||
```
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card title="Systemd Service" icon="server">
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=Weather MCP Server
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
ExecStart=/path/to/venv/bin/python /path/to/weather_server.py
|
|
||||||
Environment=OPENWEATHER_API_KEY=your-key-here
|
|
||||||
Restart=always
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
</Card>
|
|
||||||
</CardGroup>
|
|
||||||
|
|
||||||
## Next steps
|
## Next steps
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
<CardGroup cols={2}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue