Skip to content

Commit b482484

Browse files
authored
Merge branch 'main' into fix/transport-security-empty-allowlist-warning
2 parents 4f5c3ad + 2472563 commit b482484

73 files changed

Lines changed: 14344 additions & 110 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ dev = [
7878
# We add mcp[cli,ws] so `uv sync` considers the extras.
7979
"mcp[cli,ws]",
8080
"pyright>=1.1.400",
81-
"pytest>=8.3.4",
81+
"pytest>=8.4.0",
8282
"ruff>=0.8.5",
8383
"trio>=0.26.2",
8484
"pytest-flakefinder>=1.1.0",
@@ -193,6 +193,9 @@ strict-no-cover = { git = "https://github.com/pydantic/strict-no-cover" }
193193
[tool.pytest.ini_options]
194194
log_cli = true
195195
xfail_strict = true
196+
markers = [
197+
"requirement(id): links a test to the entry in tests/interaction/_requirements.py it exercises",
198+
]
196199
addopts = """
197200
--color=yes
198201
--capture=fd

src/mcp/client/auth/oauth2.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -360,10 +360,10 @@ async def _perform_authorization_code_grant(self) -> tuple[str, str]:
360360
auth_code, returned_state = await self.context.callback_handler()
361361

362362
if returned_state is None or not secrets.compare_digest(returned_state, state):
363-
raise OAuthFlowError(f"State parameter mismatch: {returned_state} != {state}") # pragma: no cover
363+
raise OAuthFlowError(f"State parameter mismatch: {returned_state} != {state}")
364364

365365
if not auth_code:
366-
raise OAuthFlowError("No authorization code received") # pragma: no cover
366+
raise OAuthFlowError("No authorization code received")
367367

368368
# Return auth code and code verifier for token exchange
369369
return auth_code, pkce_params.code_verifier
@@ -452,7 +452,7 @@ async def _refresh_token(self) -> httpx.Request:
452452

453453
return httpx.Request("POST", token_url, data=refresh_data, headers=headers)
454454

455-
async def _handle_refresh_response(self, response: httpx.Response) -> bool: # pragma: no cover
455+
async def _handle_refresh_response(self, response: httpx.Response) -> bool:
456456
"""Handle token refresh response. Returns True if successful."""
457457
if response.status_code != 200:
458458
logger.warning(f"Token refresh failed: {response.status_code}")
@@ -468,12 +468,12 @@ async def _handle_refresh_response(self, response: httpx.Response) -> bool: # p
468468
await self.context.storage.set_tokens(token_response)
469469

470470
return True
471-
except ValidationError:
471+
except ValidationError: # pragma: no cover
472472
logger.exception("Invalid refresh response")
473473
self.context.clear_tokens()
474474
return False
475475

476-
async def _initialize(self) -> None: # pragma: no cover
476+
async def _initialize(self) -> None:
477477
"""Load stored tokens and client info."""
478478
self.context.current_tokens = await self.context.storage.get_tokens()
479479
self.context.client_info = await self.context.storage.get_client_info()
@@ -507,17 +507,17 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
507507
"""HTTPX auth flow integration."""
508508
async with self.context.lock:
509509
if not self._initialized:
510-
await self._initialize() # pragma: no cover
510+
await self._initialize()
511511

512512
# Capture protocol version from request headers
513513
self.context.protocol_version = request.headers.get(MCP_PROTOCOL_VERSION)
514514

515515
if not self.context.is_token_valid() and self.context.can_refresh_token():
516516
# Try to refresh token
517-
refresh_request = await self._refresh_token() # pragma: no cover
518-
refresh_response = yield refresh_request # pragma: no cover
517+
refresh_request = await self._refresh_token()
518+
refresh_response = yield refresh_request
519519

520-
if not await self._handle_refresh_response(refresh_response): # pragma: no cover
520+
if not await self._handle_refresh_response(refresh_response):
521521
# Refresh failed, need full re-authentication
522522
self._initialized = False
523523

@@ -612,7 +612,7 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
612612
# Step 5: Perform authorization and complete token exchange
613613
token_response = yield await self._perform_authorization()
614614
await self._handle_token_response(token_response)
615-
except Exception: # pragma: no cover
615+
except Exception:
616616
logger.exception("OAuth flow error")
617617
raise
618618

src/mcp/client/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,4 +305,4 @@ async def list_tools(self, *, cursor: str | None = None, meta: RequestParamsMeta
305305
async def send_roots_list_changed(self) -> None:
306306
"""Send a notification that the roots list has changed."""
307307
# TODO(Marcelo): Currently, there is no way for the server to handle this. We should add support.
308-
await self.session.send_roots_list_changed() # pragma: no cover
308+
await self.session.send_roots_list_changed()

src/mcp/client/session.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ async def _default_elicitation_callback(
7474
context: RequestContext[ClientSession],
7575
params: types.ElicitRequestParams,
7676
) -> types.ElicitResult | types.ErrorData:
77-
return types.ErrorData( # pragma: no cover
77+
return types.ErrorData(
7878
code=types.INVALID_REQUEST,
7979
message="Elicitation not supported",
8080
)
@@ -337,9 +337,7 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) -
337337
from jsonschema import SchemaError, ValidationError, validate
338338

339339
if result.structured_content is None:
340-
raise RuntimeError(
341-
f"Tool {name} has an output schema but did not return structured content"
342-
) # pragma: no cover
340+
raise RuntimeError(f"Tool {name} has an output schema but did not return structured content")
343341
try:
344342
validate(result.structured_content, output_schema)
345343
except ValidationError as e:
@@ -408,7 +406,7 @@ async def list_tools(self, *, params: types.PaginatedRequestParams | None = None
408406

409407
return result
410408

411-
async def send_roots_list_changed(self) -> None: # pragma: no cover
409+
async def send_roots_list_changed(self) -> None:
412410
"""Send a roots/list_changed notification."""
413411
await self.send_notification(types.RootsListChangedNotification())
414412

@@ -449,7 +447,7 @@ async def _received_request(self, responder: RequestResponder[types.ServerReques
449447
client_response = ClientResponse.validate_python(response)
450448
await responder.respond(client_response)
451449

452-
case types.PingRequest(): # pragma: no cover
450+
case types.PingRequest():
453451
with responder:
454452
return await responder.respond(types.EmptyResult())
455453

src/mcp/client/streamable_http.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ async def handle_get_stream(self, client: httpx.AsyncClient, read_stream_writer:
210210
# Stream ended normally (server closed) - reset attempt counter
211211
attempt = 0
212212

213-
except Exception: # pragma: lax no cover
213+
except Exception:
214214
logger.debug("GET stream error", exc_info=True)
215215
attempt += 1
216216

@@ -267,8 +267,8 @@ async def _handle_post_request(self, ctx: RequestContext) -> None:
267267
logger.debug("Received 202 Accepted")
268268
return
269269

270-
if response.status_code == 404: # pragma: no branch
271-
if isinstance(message, JSONRPCRequest): # pragma: no branch
270+
if response.status_code == 404:
271+
if isinstance(message, JSONRPCRequest):
272272
error_data = ErrorData(code=INVALID_REQUEST, message="Session terminated")
273273
session_message = SessionMessage(JSONRPCError(jsonrpc="2.0", id=message.id, error=error_data))
274274
await ctx.read_stream_writer.send(session_message)
@@ -492,17 +492,17 @@ async def handle_request_async():
492492

493493
async def terminate_session(self, client: httpx.AsyncClient) -> None:
494494
"""Terminate the session by sending a DELETE request."""
495-
if not self.session_id: # pragma: lax no cover
496-
return
495+
if not self.session_id:
496+
return # pragma: no cover
497497

498498
try:
499499
headers = self._prepare_headers()
500500
response = await client.delete(self.url, headers=headers)
501501

502-
if response.status_code == 405: # pragma: lax no cover
502+
if response.status_code == 405:
503503
logger.debug("Server does not allow session termination")
504-
elif response.status_code not in (200, 204): # pragma: lax no cover
505-
logger.warning(f"Session termination failed: {response.status_code}")
504+
elif response.status_code not in (200, 204):
505+
logger.warning(f"Session termination failed: {response.status_code}") # pragma: no cover
506506
except Exception as exc: # pragma: no cover
507507
logger.warning(f"Session termination failed: {exc}")
508508

src/mcp/server/auth/handlers/authorize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ async def error_response(
117117
pass
118118

119119
# the error response MUST contain the state specified by the client, if any
120-
if state is None: # pragma: no cover
120+
if state is None:
121121
# make last-ditch effort to load state
122122
state = best_effort_extract_string("state", params)
123123

src/mcp/server/auth/middleware/bearer_auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ async def _send_auth_error(self, send: Send, status_code: int, error: str, descr
9595
"""Send an authentication error response with WWW-Authenticate header."""
9696
# Build WWW-Authenticate header value
9797
www_auth_parts = [f'error="{error}"', f'error_description="{description}"']
98-
if self.resource_metadata_url: # pragma: no cover
98+
if self.resource_metadata_url:
9999
www_auth_parts.append(f'resource_metadata="{self.resource_metadata_url}"')
100100

101101
www_authenticate = f"Bearer {', '.join(www_auth_parts)}"

src/mcp/server/lowlevel/server.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -349,12 +349,12 @@ def session_manager(self) -> StreamableHTTPSessionManager:
349349
Raises:
350350
RuntimeError: If called before streamable_http_app() has been called.
351351
"""
352-
if self._session_manager is None: # pragma: no cover
353-
raise RuntimeError(
352+
if self._session_manager is None:
353+
raise RuntimeError( # pragma: no cover
354354
"Session manager can only be accessed after calling streamable_http_app(). "
355355
"The session manager is created lazily to avoid unnecessary initialization."
356356
)
357-
return self._session_manager # pragma: no cover
357+
return self._session_manager
358358

359359
async def run(
360360
self,
@@ -513,7 +513,7 @@ async def _handle_request(
513513
if raise_exceptions: # pragma: no cover
514514
raise err
515515
response = types.ErrorData(code=0, message=str(err))
516-
else: # pragma: no cover
516+
else:
517517
response = types.ErrorData(code=types.METHOD_NOT_FOUND, message="Method not found")
518518

519519
if isinstance(response, types.ErrorData) and span is not None:
@@ -603,7 +603,7 @@ def streamable_http_app(
603603
required_scopes: list[str] = []
604604

605605
# Set up auth if configured
606-
if auth: # pragma: no cover
606+
if auth:
607607
required_scopes = auth.required_scopes or []
608608

609609
# Add auth middleware if token verifier is available
@@ -629,10 +629,10 @@ def streamable_http_app(
629629
)
630630

631631
# Set up routes with or without auth
632-
if token_verifier: # pragma: no cover
632+
if token_verifier:
633633
# Determine resource metadata URL
634634
resource_metadata_url = None
635-
if auth and auth.resource_server_url:
635+
if auth and auth.resource_server_url: # pragma: no branch
636636
# Build compliant metadata URL for WWW-Authenticate header
637637
resource_metadata_url = build_resource_metadata_url(auth.resource_server_url)
638638

@@ -652,7 +652,7 @@ def streamable_http_app(
652652
)
653653

654654
# Add protected resource metadata endpoint if configured as RS
655-
if auth and auth.resource_server_url: # pragma: no cover
655+
if auth and auth.resource_server_url:
656656
routes.extend(
657657
create_protected_resource_routes(
658658
resource_url=auth.resource_server_url,

src/mcp/server/mcpserver/context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ async def report_progress(self, progress: float, total: float | None = None, mes
9494
"""
9595
progress_token = self.request_context.meta.get("progress_token") if self.request_context.meta else None
9696

97-
if progress_token is None: # pragma: no cover
97+
if progress_token is None:
9898
return
9999

100100
await self.request_context.session.send_progress_notification(
@@ -242,7 +242,7 @@ async def close_sse_stream(self) -> None:
242242
This is a no-op if not using StreamableHTTP transport with event_store.
243243
The callback is only available when event_store is configured.
244244
"""
245-
if self._request_context and self._request_context.close_sse_stream: # pragma: no cover
245+
if self._request_context and self._request_context.close_sse_stream: # pragma: no branch
246246
await self._request_context.close_sse_stream()
247247

248248
async def close_standalone_sse_stream(self) -> None:

src/mcp/server/mcpserver/prompts/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,5 +185,5 @@ async def render(
185185
raise ValueError(f"Could not convert prompt result to message: {msg}")
186186

187187
return messages
188-
except Exception as e: # pragma: no cover
188+
except Exception as e:
189189
raise ValueError(f"Error rendering prompt {self.name}: {e}")

0 commit comments

Comments
 (0)