Skip to content

Commit 1df6f8b

Browse files
Merge pull request modelcontextprotocol#34 from graingert/patch-1
use httpx.AsyncClient for async http requests
2 parents 7d50d22 + 71bb5ba commit 1df6f8b

File tree

1 file changed

+62
-40
lines changed

1 file changed

+62
-40
lines changed

docs/first-server/python.mdx

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
4242

4343
<Step title="Install additional dependencies">
4444
```bash
45-
uv add requests python-dotenv
45+
uv add httpx python-dotenv
4646
```
4747
</Step>
4848

@@ -69,7 +69,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
6969
from functools import lru_cache
7070
from typing import Any
7171

72-
import requests
72+
import httpx
7373
import asyncio
7474
from dotenv import load_dotenv
7575
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
108108
Add this functionality:
109109

110110
```python
111-
# Create reusable session
112-
http = requests.Session()
113-
http.params = {
111+
# Create reusable params
112+
http_params = {
114113
"appid": API_KEY,
115114
"units": "metric"
116115
}
117116

118117
async def fetch_weather(city: str) -> dict[str, Any]:
119-
response = http.get(
120-
f"{API_BASE_URL}/weather",
121-
params={"q": city}
122-
)
123-
response.raise_for_status()
124-
data = response.json()
118+
async with httpx.AsyncClient() as client:
119+
response = await client.get(
120+
f"{API_BASE_URL}/weather",
121+
params={"q": city, **http_params}
122+
)
123+
response.raise_for_status()
124+
data = response.json()
125125

126126
return {
127127
"temperature": data["main"]["temp"],
@@ -167,7 +167,7 @@ Let's build your first MCP server in Python! We'll create a weather server that
167167
try:
168168
weather_data = await fetch_weather(city)
169169
return json.dumps(weather_data, indent=2)
170-
except requests.RequestException as e:
170+
except httpx.HTTPError as e:
171171
raise RuntimeError(f"Weather API error: {str(e)}")
172172

173173
```
@@ -220,15 +220,17 @@ Let's build your first MCP server in Python! We'll create a weather server that
220220
days = min(int(arguments.get("days", 3)), 5)
221221

222222
try:
223-
response = http.get(
224-
f"{API_BASE_URL}/{FORECAST_ENDPOINT}",
225-
params={
226-
"q": city,
227-
"cnt": days * 8 # API returns 3-hour intervals
228-
}
229-
)
230-
response.raise_for_status()
231-
data = response.json()
223+
async with httpx.AsyncClient() as client:
224+
response = await client.get(
225+
f"{API_BASE_URL}/{FORECAST_ENDPOINT}",
226+
params={
227+
"q": city,
228+
"cnt": days * 8, # API returns 3-hour intervals
229+
**http_params,
230+
}
231+
)
232+
response.raise_for_status()
233+
data = response.json()
232234

233235
forecasts = []
234236
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
245247
text=json.dumps(forecasts, indent=2)
246248
)
247249
]
248-
except requests.RequestException as e:
250+
except requests.HTTPError as e:
249251
logger.error(f"Weather API error: {str(e)}")
250252
raise RuntimeError(f"Weather API error: {str(e)}")
251253
```
@@ -443,9 +445,10 @@ Let's build your first MCP server in Python! We'll create a weather server that
443445
<Card title="Error Handling" icon="shield">
444446
```python
445447
try:
446-
response = self.http.get(...)
447-
response.raise_for_status()
448-
except requests.RequestException as e:
448+
async with httpx.AsyncClient() as client:
449+
response = await client.get(..., params={..., **http_params})
450+
response.raise_for_status()
451+
except requests.HTTPError as e:
449452
raise McpError(
450453
ErrorCode.INTERNAL_ERROR,
451454
f"API error: {str(e)}"
@@ -565,12 +568,13 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
565568
last_cache_time is None or
566569
now - last_cache_time > cache_timeout):
567570

568-
response = http.get(
569-
f"{API_BASE_URL}/{CURRENT_WEATHER_ENDPOINT}",
570-
params={"q": city}
571-
)
572-
response.raise_for_status()
573-
data = response.json()
571+
async with httpx.AsyncClient() as client:
572+
response = await client.get(
573+
f"{API_BASE_URL}/{CURRENT_WEATHER_ENDPOINT}",
574+
params={"q": city, **http_params}
575+
)
576+
response.raise_for_status()
577+
data = response.json()
574578

575579
cached_weather = {
576580
"temperature": data["main"]["temp"],
@@ -674,6 +678,10 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
674678
DEFAULT_CITY
675679
)
676680

681+
@pytest.fixture
682+
def anyio_backend():
683+
return "asyncio"
684+
677685
@pytest.fixture
678686
def mock_weather_response():
679687
return {
@@ -706,7 +714,7 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
706714
]
707715
}
708716

709-
@pytest.mark.asyncio
717+
@pytest.mark.anyio
710718
async def test_fetch_weather(mock_weather_response):
711719
with patch('requests.Session.get') as mock_get:
712720
mock_get.return_value.json.return_value = mock_weather_response
@@ -720,7 +728,7 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
720728
assert weather["wind_speed"] == 3.6
721729
assert "timestamp" in weather
722730

723-
@pytest.mark.asyncio
731+
@pytest.mark.anyio
724732
async def test_read_resource():
725733
with patch('weather_service.server.fetch_weather') as mock_fetch:
726734
mock_fetch.return_value = {
@@ -736,12 +744,26 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
736744
assert "temperature" in result
737745
assert "clear sky" in result
738746

739-
@pytest.mark.asyncio
747+
@pytest.mark.anyio
740748
async def test_call_tool(mock_forecast_response):
741-
with patch('weather_service.server.http.get') as mock_get:
742-
mock_get.return_value.json.return_value = mock_forecast_response
743-
mock_get.return_value.raise_for_status = Mock()
749+
class Response():
750+
def raise_for_status(self):
751+
pass
752+
753+
def json(self):
754+
return nock_forecast_response
755+
756+
class AsyncClient():
757+
def __aenter__(self):
758+
return self
759+
760+
async def __aexit__(self, *exc_info):
761+
pass
762+
763+
async def get(self, *args, **kwargs):
764+
return Response()
744765

766+
with patch('httpx.AsyncClient', new=AsyncClient) as mock_client:
745767
result = await call_tool("get_forecast", {"city": "London", "days": 2})
746768

747769
assert len(result) == 1
@@ -751,14 +773,14 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
751773
assert forecast_data[0]["temperature"] == 18.5
752774
assert forecast_data[0]["conditions"] == "sunny"
753775

754-
@pytest.mark.asyncio
776+
@pytest.mark.anyio
755777
async def test_list_resources():
756778
resources = await list_resources()
757779
assert len(resources) == 1
758780
assert resources[0].name == f"Current weather in {DEFAULT_CITY}"
759781
assert resources[0].mimeType == "application/json"
760782

761-
@pytest.mark.asyncio
783+
@pytest.mark.anyio
762784
async def test_list_tools():
763785
tools = await list_tools()
764786
assert len(tools) == 1
@@ -768,7 +790,7 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
768790
</Step>
769791
<Step title="Run tests">
770792
```bash
771-
uv add --dev pytest pytest-asyncio
793+
uv add --dev pytest
772794
uv run pytest
773795
```
774796
</Step>

0 commit comments

Comments
 (0)