Skip to content

Commit b19d830

Browse files
committed
docs: assert CORS behavior, not starlette internals; drop pragmas the docs tests cover
The lowest-direct CI cells failed on tests/docs_src/test_asgi.py reaching into starlette's `Middleware` internals — `.kwargs` does not exist on the oldest supported starlette. The test now drives the app over `httpx.ASGITransport` and asserts what the page promises a browser: the preflight allows GET/POST/DELETE and a cross-origin response exposes `Mcp-Session-Id`. The locked cells failed at `strict-no-cover`: the docs tests execute 21 lines marked `# pragma: no cover` — the `Image`/`Audio` helper paths, the elicitation cancel arm, custom Starlette routes on the low-level app, `Context.request_context` outside a request, and the token_verifier-without-auth `ValueError`. Those markers are no longer true, so they are removed; the full suite still reports 100% under branch coverage.
1 parent 6ae3efa commit b19d830

6 files changed

Lines changed: 25 additions & 15 deletions

File tree

src/mcp/server/elicitation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ async def elicit_with_validation(
113113
return AcceptedElicitation(data=validated_data)
114114
elif result.action == "decline":
115115
return DeclinedElicitation()
116-
elif result.action == "cancel": # pragma: no cover
116+
elif result.action == "cancel":
117117
return CancelledElicitation()
118118
else: # pragma: no cover
119119
# This should never happen, but handle it just in case

src/mcp/server/lowlevel/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ def streamable_http_app(
561561
)
562562
)
563563

564-
if custom_starlette_routes: # pragma: no cover
564+
if custom_starlette_routes:
565565
routes.extend(custom_starlette_routes)
566566

567567
return Starlette(

src/mcp/server/mcpserver/context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def mcp_server(self) -> MCPServer:
8282
@property
8383
def request_context(self) -> ServerRequestContext[LifespanContextT, RequestT]:
8484
"""Access to the underlying request context."""
85-
if self._request_context is None: # pragma: no cover
85+
if self._request_context is None:
8686
raise ValueError("Context is not available outside of a request")
8787
return self._request_context
8888

src/mcp/server/mcpserver/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def __init__(
192192
raise ValueError("Cannot specify both auth_server_provider and token_verifier")
193193
if not auth_server_provider and not token_verifier: # pragma: no cover
194194
raise ValueError("Must specify either auth_server_provider or token_verifier when auth is enabled")
195-
elif auth_server_provider or token_verifier: # pragma: no cover
195+
elif auth_server_provider or token_verifier:
196196
raise ValueError("Cannot specify auth_server_provider or token_verifier without auth settings")
197197

198198
self._auth_server_provider = auth_server_provider
@@ -821,15 +821,15 @@ async def health_check(request: Request) -> Response:
821821
```
822822
"""
823823

824-
def decorator( # pragma: no cover
824+
def decorator(
825825
func: Callable[[Request], Awaitable[Response]],
826826
) -> Callable[[Request], Awaitable[Response]]:
827827
self._custom_starlette_routes.append(
828828
Route(path, endpoint=func, methods=methods, name=name, include_in_schema=include_in_schema)
829829
)
830830
return func
831831

832-
return decorator # pragma: no cover
832+
return decorator
833833

834834
async def run_stdio_async(self) -> None:
835835
"""Run the server using stdio transport."""

src/mcp/server/mcpserver/utilities/types.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(
2727

2828
def _get_mime_type(self) -> str:
2929
"""Get MIME type from format or guess from file extension."""
30-
if self._format: # pragma: no cover
30+
if self._format:
3131
return f"image/{self._format.lower()}"
3232

3333
if self.path:
@@ -39,14 +39,14 @@ def _get_mime_type(self) -> str:
3939
".gif": "image/gif",
4040
".webp": "image/webp",
4141
}.get(suffix, "application/octet-stream")
42-
return "image/png" # pragma: no cover # default for raw binary data
42+
return "image/png" # default for raw binary data
4343

4444
def to_image_content(self) -> ImageContent:
4545
"""Convert to MCP ImageContent."""
4646
if self.path:
4747
with open(self.path, "rb") as f:
4848
data = base64.b64encode(f.read()).decode()
49-
elif self.data is not None: # pragma: no cover
49+
elif self.data is not None:
5050
data = base64.b64encode(self.data).decode()
5151
else: # pragma: no cover
5252
raise ValueError("No image data available")
@@ -73,7 +73,7 @@ def __init__(
7373

7474
def _get_mime_type(self) -> str:
7575
"""Get MIME type from format or guess from file extension."""
76-
if self._format: # pragma: no cover
76+
if self._format:
7777
return f"audio/{self._format.lower()}"
7878

7979
if self.path:
@@ -86,14 +86,14 @@ def _get_mime_type(self) -> str:
8686
".aac": "audio/aac",
8787
".m4a": "audio/mp4",
8888
}.get(suffix, "application/octet-stream")
89-
return "audio/wav" # pragma: no cover # default for raw binary data
89+
return "audio/wav" # default for raw binary data
9090

9191
def to_audio_content(self) -> AudioContent:
9292
"""Convert to MCP AudioContent."""
9393
if self.path:
9494
with open(self.path, "rb") as f:
9595
data = base64.b64encode(f.read()).decode()
96-
elif self.data is not None: # pragma: no cover
96+
elif self.data is not None:
9797
data = base64.b64encode(self.data).decode()
9898
else: # pragma: no cover
9999
raise ValueError("No audio data available")

tests/docs_src/test_asgi.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,21 @@ async def test_streamable_http_path_moves_the_endpoint_to_the_mount_prefix() ->
124124

125125

126126
async def test_cors_exposes_the_session_id_header() -> None:
127-
"""tutorial005: the CORS middleware exposes `Mcp-Session-Id` and allows the three MCP methods."""
127+
"""tutorial005: the browser origin gets the three MCP methods and can read `Mcp-Session-Id`."""
128128
(middleware,) = tutorial005.app.user_middleware
129129
assert middleware.cls is CORSMiddleware
130-
assert middleware.kwargs["expose_headers"] == ["Mcp-Session-Id"]
131-
assert middleware.kwargs["allow_methods"] == ["GET", "POST", "DELETE"]
130+
transport = httpx.ASGITransport(app=tutorial005.app)
131+
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1") as http:
132+
preflight = await http.options(
133+
"/mcp",
134+
headers={"Origin": "https://app.example.com", "Access-Control-Request-Method": "POST"},
135+
)
136+
assert preflight.status_code == 200
137+
assert preflight.headers["access-control-allow-methods"] == "GET, POST, DELETE"
138+
139+
response = await http.get("/not-the-endpoint", headers={"Origin": "https://app.example.com"})
140+
assert response.headers["access-control-allow-origin"] == "https://app.example.com"
141+
assert response.headers["access-control-expose-headers"] == "Mcp-Session-Id"
132142

133143

134144
async def test_custom_route_lands_next_to_the_mcp_endpoint() -> None:

0 commit comments

Comments
 (0)