Skip to content

Commit 8786667

Browse files
authored
Merge branch 'main' into enum_updates
2 parents 8728fe2 + 47d35f0 commit 8786667

File tree

6 files changed

+147
-6
lines changed

6 files changed

+147
-6
lines changed

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,32 @@
3131
- [Prompts](#prompts)
3232
- [Images](#images)
3333
- [Context](#context)
34+
- [Getting Context in Functions](#getting-context-in-functions)
35+
- [Context Properties and Methods](#context-properties-and-methods)
3436
- [Completions](#completions)
3537
- [Elicitation](#elicitation)
3638
- [Sampling](#sampling)
3739
- [Logging and Notifications](#logging-and-notifications)
3840
- [Authentication](#authentication)
3941
- [FastMCP Properties](#fastmcp-properties)
40-
- [Session Properties](#session-properties-and-methods)
42+
- [Session Properties and Methods](#session-properties-and-methods)
4143
- [Request Context Properties](#request-context-properties)
4244
- [Running Your Server](#running-your-server)
4345
- [Development Mode](#development-mode)
4446
- [Claude Desktop Integration](#claude-desktop-integration)
4547
- [Direct Execution](#direct-execution)
4648
- [Streamable HTTP Transport](#streamable-http-transport)
49+
- [CORS Configuration for Browser-Based Clients](#cors-configuration-for-browser-based-clients)
4750
- [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server)
51+
- [StreamableHTTP servers](#streamablehttp-servers)
52+
- [Basic mounting](#basic-mounting)
53+
- [Host-based routing](#host-based-routing)
54+
- [Multiple servers with path configuration](#multiple-servers-with-path-configuration)
55+
- [Path configuration at initialization](#path-configuration-at-initialization)
56+
- [SSE servers](#sse-servers)
4857
- [Advanced Usage](#advanced-usage)
4958
- [Low-Level Server](#low-level-server)
59+
- [Structured Output Support](#structured-output-support)
5060
- [Writing MCP Clients](#writing-mcp-clients)
5161
- [Client Display Utilities](#client-display-utilities)
5262
- [OAuth Authentication for Clients](#oauth-authentication-for-clients)
@@ -400,7 +410,7 @@ def get_weather(city: str) -> WeatherData:
400410
"""Get weather for a city - returns structured data."""
401411
# Simulated weather data
402412
return WeatherData(
403-
temperature=72.5,
413+
temperature=22.5,
404414
humidity=45.0,
405415
condition="sunny",
406416
wind_speed=5.2,
@@ -2137,6 +2147,7 @@ MCP servers declare capabilities during initialization:
21372147

21382148
## Documentation
21392149

2150+
- [API Reference](https://modelcontextprotocol.github.io/python-sdk/api/)
21402151
- [Model Context Protocol documentation](https://modelcontextprotocol.io)
21412152
- [Model Context Protocol specification](https://spec.modelcontextprotocol.io)
21422153
- [Officially supported servers](https://github.com/modelcontextprotocol/servers)

examples/servers/simple-resource/mcp_simple_resource/server.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import click
33
import mcp.types as types
44
from mcp.server.lowlevel import Server
5+
from mcp.server.lowlevel.helper_types import ReadResourceContents
56
from pydantic import AnyUrl, FileUrl
67
from starlette.requests import Request
78

@@ -46,15 +47,15 @@ async def list_resources() -> list[types.Resource]:
4647
]
4748

4849
@app.read_resource()
49-
async def read_resource(uri: AnyUrl) -> str | bytes:
50+
async def read_resource(uri: AnyUrl):
5051
if uri.path is None:
5152
raise ValueError(f"Invalid resource path: {uri}")
5253
name = uri.path.replace(".txt", "").lstrip("/")
5354

5455
if name not in SAMPLE_RESOURCES:
5556
raise ValueError(f"Unknown resource: {uri}")
5657

57-
return SAMPLE_RESOURCES[name]["content"]
58+
return [ReadResourceContents(content=SAMPLE_RESOURCES[name]["content"], mime_type="text/plain")]
5859

5960
if transport == "sse":
6061
from mcp.server.sse import SseServerTransport

examples/snippets/servers/structured_output.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def get_weather(city: str) -> WeatherData:
2424
"""Get weather for a city - returns structured data."""
2525
# Simulated weather data
2626
return WeatherData(
27-
temperature=72.5,
27+
temperature=22.5,
2828
humidity=45.0,
2929
condition="sunny",
3030
wind_speed=5.2,

src/mcp/server/session.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ async def _received_request(self, responder: RequestResponder[types.ClientReques
161161
)
162162
)
163163
)
164+
case types.PingRequest():
165+
# Ping requests are allowed at any time
166+
pass
164167
case _:
165168
if self._initialization_state != InitializationState.Initialized:
166169
raise RuntimeError("Received request before initialization was complete")

tests/server/fastmcp/test_integration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ async def test_structured_output(server_transport: str, server_url: str) -> None
693693

694694
# Check that the result contains expected weather data
695695
result_text = weather_result.content[0].text
696-
assert "72.5" in result_text # temperature
696+
assert "22.5" in result_text # temperature
697697
assert "sunny" in result_text # condition
698698
assert "45" in result_text # humidity
699699
assert "5.2" in result_text # wind_speed

tests/server/test_session.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,129 @@ async def mock_client():
213213

214214
assert received_initialized
215215
assert received_protocol_version == "2024-11-05"
216+
217+
218+
@pytest.mark.anyio
219+
async def test_ping_request_before_initialization():
220+
"""Test that ping requests are allowed before initialization is complete."""
221+
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)
222+
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)
223+
224+
ping_response_received = False
225+
ping_response_id = None
226+
227+
async def run_server():
228+
async with ServerSession(
229+
client_to_server_receive,
230+
server_to_client_send,
231+
InitializationOptions(
232+
server_name="mcp",
233+
server_version="0.1.0",
234+
capabilities=ServerCapabilities(),
235+
),
236+
) as server_session:
237+
async for message in server_session.incoming_messages:
238+
if isinstance(message, Exception):
239+
raise message
240+
241+
# We should receive a ping request before initialization
242+
if isinstance(message, RequestResponder) and isinstance(message.request.root, types.PingRequest):
243+
# Respond to the ping
244+
with message:
245+
await message.respond(types.ServerResult(types.EmptyResult()))
246+
return
247+
248+
async def mock_client():
249+
nonlocal ping_response_received, ping_response_id
250+
251+
# Send ping request before any initialization
252+
await client_to_server_send.send(
253+
SessionMessage(
254+
types.JSONRPCMessage(
255+
types.JSONRPCRequest(
256+
jsonrpc="2.0",
257+
id=42,
258+
method="ping",
259+
)
260+
)
261+
)
262+
)
263+
264+
# Wait for the ping response
265+
ping_response_message = await server_to_client_receive.receive()
266+
assert isinstance(ping_response_message.message.root, types.JSONRPCResponse)
267+
268+
ping_response_received = True
269+
ping_response_id = ping_response_message.message.root.id
270+
271+
async with (
272+
client_to_server_send,
273+
client_to_server_receive,
274+
server_to_client_send,
275+
server_to_client_receive,
276+
anyio.create_task_group() as tg,
277+
):
278+
tg.start_soon(run_server)
279+
tg.start_soon(mock_client)
280+
281+
assert ping_response_received
282+
assert ping_response_id == 42
283+
284+
285+
@pytest.mark.anyio
286+
async def test_other_requests_blocked_before_initialization():
287+
"""Test that non-ping requests are still blocked before initialization."""
288+
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)
289+
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)
290+
291+
error_response_received = False
292+
error_code = None
293+
294+
async def run_server():
295+
async with ServerSession(
296+
client_to_server_receive,
297+
server_to_client_send,
298+
InitializationOptions(
299+
server_name="mcp",
300+
server_version="0.1.0",
301+
capabilities=ServerCapabilities(),
302+
),
303+
):
304+
# Server should handle the request and send an error response
305+
# No need to process incoming_messages since the error is handled automatically
306+
await anyio.sleep(0.1) # Give time for the request to be processed
307+
308+
async def mock_client():
309+
nonlocal error_response_received, error_code
310+
311+
# Try to send a non-ping request before initialization
312+
await client_to_server_send.send(
313+
SessionMessage(
314+
types.JSONRPCMessage(
315+
types.JSONRPCRequest(
316+
jsonrpc="2.0",
317+
id=1,
318+
method="prompts/list",
319+
)
320+
)
321+
)
322+
)
323+
324+
# Wait for the error response
325+
error_message = await server_to_client_receive.receive()
326+
if isinstance(error_message.message.root, types.JSONRPCError):
327+
error_response_received = True
328+
error_code = error_message.message.root.error.code
329+
330+
async with (
331+
client_to_server_send,
332+
client_to_server_receive,
333+
server_to_client_send,
334+
server_to_client_receive,
335+
anyio.create_task_group() as tg,
336+
):
337+
tg.start_soon(run_server)
338+
tg.start_soon(mock_client)
339+
340+
assert error_response_received
341+
assert error_code == types.INVALID_PARAMS

0 commit comments

Comments
 (0)