Skip to content

Commit e805368

Browse files
authored
Merge branch 'main' into fix/httpx-client-default-timeout-docstring
2 parents 52c9bc5 + a5b2ebb commit e805368

83 files changed

Lines changed: 15010 additions & 145 deletions

File tree

Some content is hidden

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

‎examples/servers/simple-auth/mcp_simple_auth/auth_server.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ async def introspect_handler(request: Request) -> Response:
120120
"iat": int(time.time()),
121121
"token_type": "Bearer",
122122
"aud": access_token.resource, # RFC 8707 audience claim
123+
"sub": access_token.subject, # RFC 7662 subject
124+
"iss": str(server_settings.server_url),
123125
}
124126
)
125127

‎examples/servers/simple-auth/mcp_simple_auth/simple_auth_provider.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ async def handle_simple_callback(self, username: str, password: str, state: str)
181181
scopes=[self.settings.mcp_scope],
182182
code_challenge=code_challenge,
183183
resource=resource, # RFC 8707
184+
subject=username,
184185
)
185186
self.auth_codes[new_code] = auth_code
186187

@@ -219,6 +220,7 @@ async def exchange_authorization_code(
219220
scopes=authorization_code.scopes,
220221
expires_at=int(time.time()) + 3600,
221222
resource=authorization_code.resource, # RFC 8707
223+
subject=authorization_code.subject,
222224
)
223225

224226
# Store user data mapping for this token

‎examples/servers/simple-auth/mcp_simple_auth/token_verifier.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ async def verify_token(self, token: str) -> AccessToken | None:
7575
scopes=data.get("scope", "").split() if data.get("scope") else [],
7676
expires_at=data.get("exp"),
7777
resource=data.get("aud"), # Include resource in token
78+
subject=data.get("sub"), # RFC 7662 subject (resource owner)
79+
claims=data,
7880
)
7981
except Exception as e:
8082
logger.warning(f"Token introspection failed: {e}")

‎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/cli/cli.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ def dev(
277277
[npx_cmd, "@modelcontextprotocol/inspector"] + uv_cmd,
278278
check=True,
279279
shell=shell,
280-
env=dict(os.environ.items()), # Convert to list of tuples for env update
280+
env=dict(os.environ.items()), # Copy the environment for subprocess launch
281281
)
282282
sys.exit(process.returncode)
283283
except subprocess.CalledProcessError as e:

‎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

0 commit comments

Comments
 (0)