Merge pull request #34 from graingert/patch-1

use httpx.AsyncClient for async http requests
This commit is contained in:
Justin Spahr-Summers 2024-11-25 15:50:04 -06:00 • committed by GitHub
commit 1df6f8b33c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -42,7 +42,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
<Step title="Install additional dependencies"> <Step title="Install additional dependencies">
```bash ```bash
uv add requests python-dotenv uv add httpx python-dotenv
``` ```
</Step> </Step>
@ -69,7 +69,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
from functools import lru_cache from functools import lru_cache
from typing import Any from typing import Any
import requests import httpx
import asyncio import asyncio
from dotenv import load_dotenv from dotenv import load_dotenv
from mcp.server import Server from mcp.server import Server
@ -108,20 +108,20 @@ Let's build your first MCP server in Python! We'll create a weather server that
Add this functionality: Add this functionality:
```python ```python
# Create reusable session # Create reusable params
http = requests.Session() http_params = {
http.params = {
"appid": API_KEY, "appid": API_KEY,
"units": "metric" "units": "metric"
} }
async def fetch_weather(city: str) -> dict[str, Any]: async def fetch_weather(city: str) -> dict[str, Any]:
response = http.get( async with httpx.AsyncClient() as client:
f"{API_BASE_URL}/weather", response = await client.get(
params={"q": city} f"{API_BASE_URL}/weather",
) params={"q": city, **http_params}
response.raise_for_status() )
data = response.json() response.raise_for_status()
data = response.json()
return { return {
"temperature": data["main"]["temp"], "temperature": data["main"]["temp"],
@ -167,7 +167,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
try: try:
weather_data = await fetch_weather(city) weather_data = await fetch_weather(city)
return json.dumps(weather_data, indent=2) return json.dumps(weather_data, indent=2)
except requests.RequestException as e: except httpx.HTTPError as e:
raise RuntimeError(f"Weather API error: {str(e)}") raise RuntimeError(f"Weather API error: {str(e)}")
``` ```
@ -220,15 +220,17 @@ Let's build your first MCP server in Python! We'll create a weather server that
days = min(int(arguments.get("days", 3)), 5) days = min(int(arguments.get("days", 3)), 5)
try: try:
response = http.get( async with httpx.AsyncClient() as client:
f"{API_BASE_URL}/{FORECAST_ENDPOINT}", response = await client.get(
params={ f"{API_BASE_URL}/{FORECAST_ENDPOINT}",
"q": city, params={
"cnt": days * 8 # API returns 3-hour intervals "q": city,
} "cnt": days * 8, # API returns 3-hour intervals
) **http_params,
response.raise_for_status() }
data = response.json() )
response.raise_for_status()
data = response.json()
forecasts = [] forecasts = []
for i in range(0, len(data["list"]), 8): for i in range(0, len(data["list"]), 8):
@ -245,7 +247,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
text=json.dumps(forecasts, indent=2) text=json.dumps(forecasts, indent=2)
) )
] ]
except requests.RequestException as e: except requests.HTTPError as e:
logger.error(f"Weather API error: {str(e)}") logger.error(f"Weather API error: {str(e)}")
raise RuntimeError(f"Weather API error: {str(e)}") raise RuntimeError(f"Weather API error: {str(e)}")
``` ```
@ -443,9 +445,10 @@ 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.http.get(...) async with httpx.AsyncClient() as client:
response.raise_for_status() response = await client.get(..., params={..., **http_params})
except requests.RequestException as e: response.raise_for_status()
except requests.HTTPError as e:
raise McpError( raise McpError(
ErrorCode.INTERNAL_ERROR, ErrorCode.INTERNAL_ERROR,
f"API error: {str(e)}" f"API error: {str(e)}"
@ -565,12 +568,13 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
last_cache_time is None or last_cache_time is None or
now - last_cache_time > cache_timeout): now - last_cache_time > cache_timeout):
response = http.get( async with httpx.AsyncClient() as client:
f"{API_BASE_URL}/{CURRENT_WEATHER_ENDPOINT}", response = await client.get(
params={"q": city} f"{API_BASE_URL}/{CURRENT_WEATHER_ENDPOINT}",
) params={"q": city, **http_params}
response.raise_for_status() )
data = response.json() response.raise_for_status()
data = response.json()
cached_weather = { cached_weather = {
"temperature": data["main"]["temp"], "temperature": data["main"]["temp"],
@ -674,6 +678,10 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
DEFAULT_CITY DEFAULT_CITY
) )
@pytest.fixture
def anyio_backend():
return "asyncio"
@pytest.fixture @pytest.fixture
def mock_weather_response(): def mock_weather_response():
return { return {
@ -706,7 +714,7 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
] ]
} }
@pytest.mark.asyncio @pytest.mark.anyio
async def test_fetch_weather(mock_weather_response): async def test_fetch_weather(mock_weather_response):
with patch('requests.Session.get') as mock_get: with patch('requests.Session.get') as mock_get:
mock_get.return_value.json.return_value = mock_weather_response mock_get.return_value.json.return_value = mock_weather_response
@ -720,7 +728,7 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
assert weather["wind_speed"] == 3.6 assert weather["wind_speed"] == 3.6
assert "timestamp" in weather assert "timestamp" in weather
@pytest.mark.asyncio @pytest.mark.anyio
async def test_read_resource(): async def test_read_resource():
with patch('weather_service.server.fetch_weather') as mock_fetch: with patch('weather_service.server.fetch_weather') as mock_fetch:
mock_fetch.return_value = { mock_fetch.return_value = {
@ -736,12 +744,26 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
assert "temperature" in result assert "temperature" in result
assert "clear sky" in result assert "clear sky" in result
@pytest.mark.asyncio @pytest.mark.anyio
async def test_call_tool(mock_forecast_response): async def test_call_tool(mock_forecast_response):
with patch('weather_service.server.http.get') as mock_get: class Response():
mock_get.return_value.json.return_value = mock_forecast_response def raise_for_status(self):
mock_get.return_value.raise_for_status = Mock() pass
def json(self):
return nock_forecast_response
class AsyncClient():
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}) result = await call_tool("get_forecast", {"city": "London", "days": 2})
assert len(result) == 1 assert len(result) == 1
@ -751,14 +773,14 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
assert forecast_data[0]["temperature"] == 18.5 assert forecast_data[0]["temperature"] == 18.5
assert forecast_data[0]["conditions"] == "sunny" assert forecast_data[0]["conditions"] == "sunny"
@pytest.mark.asyncio @pytest.mark.anyio
async def test_list_resources(): async def test_list_resources():
resources = await list_resources() resources = await list_resources()
assert len(resources) == 1 assert len(resources) == 1
assert resources[0].name == f"Current weather in {DEFAULT_CITY}" assert resources[0].name == f"Current weather in {DEFAULT_CITY}"
assert resources[0].mimeType == "application/json" assert resources[0].mimeType == "application/json"
@pytest.mark.asyncio @pytest.mark.anyio
async def test_list_tools(): async def test_list_tools():
tools = await list_tools() tools = await list_tools()
assert len(tools) == 1 assert len(tools) == 1
@ -768,7 +790,7 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
</Step> </Step>
<Step title="Run tests"> <Step title="Run tests">
```bash ```bash
uv add --dev pytest pytest-asyncio uv add --dev pytest
uv run pytest uv run pytest
``` ```
</Step> </Step>