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)