Merge pull request #34 from graingert/patch-1
use httpx.AsyncClient for async http requests
This commit is contained in:
commit
1df6f8b33c
1 changed files with 62 additions and 40 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue