| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -112,7 +112,7 @@ A nightly job, a CI step, another service. There is no browser and nobody to cli | |||
| 112 | 112 | What changed: | |
| 113 | 113 | ||
| 114 | 114 | * No `OAuthClientMetadata`, no handlers. You pass `client_id` and `client_secret`; the provider builds a minimal `client_credentials` registration around them and skips dynamic registration entirely. | |
| 115 | - * `issuer` names the authorization server that issued those credentials; use the `issuer` value its `/.well-known/oauth-authorization-server` document returns. Discovery still runs as above, but token requests are only ever built from metadata for *that* issuer; if the MCP server points anywhere else, the flow stops with an `OAuthFlowError` instead. Leave it out and the provider uses whichever authorization server discovery finds. | ||
| 115 | + * `issuer` names the authorization server that issued those credentials; use the `issuer` value its `/.well-known/oauth-authorization-server` document returns. Discovery still runs as above, but token requests are only ever built from metadata for *that* issuer; if the MCP server points anywhere else, the flow stops with an `OAuthFlowError` instead. Leaving it out is deprecated and it becomes required in 3.0 (see **[Deprecated features](../deprecated.md#deprecated-sdk-helpers)**); until then the provider warns and uses whichever authorization server discovery finds. | ||
| 116 | 116 | * `scope` is a space-separated string, the OAuth wire format. | |
| 117 | 117 | * Everything downstream is identical: the same `TokenStorage`, the same `httpx2.AsyncClient(auth=...)`, the same `streamable_http_client`. | |
| 118 | 118 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,6 +1,6 @@ | |||
| 1 | 1 | # Deprecated features | |
| 2 | 2 | ||
| 3 | - The 2026-07-28 spec retires five things. The SDK still implements every one of them, and every one of them now carries a **deprecation warning**. One SDK helper is deprecated on its own account and is listed [at the end](#deprecated-sdk-helpers). | ||
| 3 | + The 2026-07-28 spec retires five things. The SDK still implements every one of them, and every one of them now carries a **deprecation warning**. A few SDK-level deprecations stand on their own account and are listed [at the end](#deprecated-sdk-helpers). | ||
| 4 | 4 | ||
| 5 | 5 | The table below names each deprecated feature, why it is going away, and the replacement to build on. | |
| 6 | 6 | ||
@@ -131,11 +131,12 @@ That is the whole API. There is no per-method switch, and you don't want one: th | |||
| 131 | 131 | ||
| 132 | 132 | ## Deprecated SDK helpers | |
| 133 | 133 | ||
| 134 | - These are not spec changes, only SDK internals with a better replacement. They warn with the same `MCPDeprecationWarning` and will be removed in 3.0. | ||
| 134 | + These are not spec changes, only SDK usage with a better replacement. They warn with the same `MCPDeprecationWarning`, and 3.0 removes the old form. | ||
| 135 | 135 | ||
| 136 | 136 | | Deprecated | What you do instead | | |
| 137 | 137 | |---|---| | |
| 138 | 138 | | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` and then `FuncMetadata.call_fn()`. Only code that drives `FuncMetadata` directly (a custom `Tool` subclass, say) ever called it. | | |
| 139 | + | `ClientCredentialsOAuthProvider(...)` or `PrivateKeyJWTOAuthProvider(...)` without `issuer=` | Pass `issuer=` naming the authorization server that issued the credentials (see **[Writing OAuth clients](client/oauth-clients.md#machine-to-machine)**). Without it the MCP server decides which authorization server receives them; 3.0 makes the keyword required. | | ||
| 139 | 140 | ||
| 140 | 141 | ## Recap | |
| 141 | 142 | ||
@@ -144,7 +145,7 @@ These are not spec changes, only SDK internals with a better replacement. They w | |||
| 144 | 145 | * Deprecated is advisory: no wire changes, everything keeps working against pre-2026 sessions, and you get a visible `MCPDeprecationWarning` (a `UserWarning`, so it is on by default). | |
| 145 | 146 | * Sampling and roots additionally need a back-channel that a 2026-07-28 session does not have. On a modern connection they warn and then they raise. | |
| 146 | 147 | * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silences the whole category; `"error::mcp.MCPDeprecationWarning"` in pytest turns it into a test failure. | |
| 147 | - * One SDK helper, `FuncMetadata.call_fn_with_arg_validation()`, is deprecated separately for removal in 3.0. | ||
| 148 | + * The [SDK-level deprecations](#deprecated-sdk-helpers) follow the same rule: they warn now, and 3.0 drops the old form. | ||
| 148 | 149 | * New code should not be built on any of these. | |
| 149 | 150 | ||
| 150 | 151 | Every other page in these docs teaches the current API. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -33,8 +33,8 @@ the client and server side. | |||
| 33 | 33 | - `client.py` `main` — opens with `async with Client(target, mode=mode) as | |
| 34 | 34 | client:` and that's the whole program. `target` is a transport that already | |
| 35 | 35 | carries the OAuth `httpx2.Auth`; the body never touches a token. | |
| 36 | - - `client.py` `build_auth` — five lines of `ClientCredentialsOAuthProvider` | ||
| 37 | - config is all the caller writes; the SDK does RFC 9728 PRM → | ||
| 36 | + - `client.py` `build_auth` — a few lines of `ClientCredentialsOAuthProvider` | ||
| 37 | + config (credentials plus `issuer=`) is all the caller writes; the SDK does RFC 9728 PRM → | ||
| 38 | 38 | RFC 8414 AS-metadata discovery and token exchange on the first 401. | |
| 39 | 39 | - `server.py` `token_endpoint` — the *entire* AS for this grant: validate | |
| 40 | 40 | HTTP-Basic `client_id:client_secret`, mint a token, return RFC 6749 JSON. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -8,13 +8,13 @@ | |||
| 8 | 8 | ||
| 9 | 9 | # MCP_URL pins the resource to :8000, and the server side builds its PRM/AS metadata from | |
| 10 | 10 | # the same constant — run the server on 8000 or the discovery chain points at the wrong origin. | |
| 11 | - from stories._shared.auth import MCP_URL, InMemoryTokenStorage | ||
| 11 | + from stories._shared.auth import BASE_URL, MCP_URL, InMemoryTokenStorage | ||
| 12 | 12 | ||
| 13 | 13 | from .server import DEMO_CLIENT_ID, DEMO_CLIENT_SECRET, DEMO_SCOPE | |
| 14 | 14 | ||
| 15 | 15 | ||
| 16 | 16 | def build_auth(_http: httpx2.AsyncClient) -> httpx2.Auth: | |
| 17 | - """The ``httpx2.Auth`` for the ``client_credentials`` grant — five lines of provider config. | ||
| 17 | + """The ``httpx2.Auth`` for the ``client_credentials`` grant — a few lines of provider config. | ||
| 18 | 18 | ||
| 19 | 19 | The SDK then handles 401 → RFC 9728 PRM → RFC 8414 AS-metadata discovery → token POST → | |
| 20 | 20 | Bearer attachment automatically. ``Client(url)`` has no ``auth=`` passthrough yet, so the | |
@@ -27,6 +27,7 @@ def build_auth(_http: httpx2.AsyncClient) -> httpx2.Auth: | |||
| 27 | 27 | client_id=DEMO_CLIENT_ID, | |
| 28 | 28 | client_secret=DEMO_CLIENT_SECRET, | |
| 29 | 29 | scope=DEMO_SCOPE, | |
| 30 | + issuer=BASE_URL, | ||
| 30 | 31 | ) | |
| 31 | 32 | ||
| 32 | 33 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -7,6 +7,7 @@ | |||
| 7 | 7 | """ | |
| 8 | 8 | ||
| 9 | 9 | import time | |
| 10 | + import warnings | ||
| 10 | 11 | from collections.abc import Awaitable, Callable | |
| 11 | 12 | from typing import Any, Literal | |
| 12 | 13 | from urllib.parse import urlparse | |
@@ -20,10 +21,20 @@ | |||
| 20 | 21 | from mcp.client.auth.oauth2 import OAuthContext | |
| 21 | 22 | from mcp.client.auth.utils import issuers_match | |
| 22 | 23 | from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata | |
| 24 | + from mcp.shared.exceptions import MCPDeprecationWarning | ||
| 23 | 25 | ||
| 24 | 26 | ||
| 25 | 27 | def _checked_issuer(issuer: str | None) -> str | None: | |
| 26 | - if issuer is not None and urlparse(issuer).scheme not in ("http", "https"): | ||
| 28 | + if issuer is None: | ||
| 29 | + warnings.warn( | ||
| 30 | + "Omitting `issuer` is deprecated and it will be required in 3.0. Without it, the MCP server " | ||
| 31 | + "decides which authorization server receives this client's credentials; pass " | ||
| 32 | + "issuer=<your authorization server's issuer URL> so they are only ever sent there.", | ||
| 33 | + MCPDeprecationWarning, | ||
| 34 | + stacklevel=3, | ||
| 35 | + ) | ||
| 36 | + return None | ||
| 37 | + if urlparse(issuer).scheme not in ("http", "https"): | ||
| 27 | 38 | raise ValueError(f"issuer must be the authorization server's http(s) issuer URL, got {issuer!r}") | |
| 28 | 39 | return issuer | |
| 29 | 40 | ||
@@ -97,7 +108,8 @@ def __init__( | |||
| 97 | 108 | issuer: The issuer identifier of the authorization server that issued | |
| 98 | 109 | `client_id` and `client_secret`. When set, token requests are only built from | |
| 99 | 110 | discovered authorization server metadata whose `issuer` is exactly this string; | |
| 100 | - otherwise the flow stops with `OAuthFlowError`. When omitted, whichever | ||
| 111 | + otherwise the flow stops with `OAuthFlowError`. Omitting it is deprecated | ||
| 112 | + (`MCPDeprecationWarning`) and it will be required in 3.0; until then, whichever | ||
| 101 | 113 | authorization server discovery yields is used. | |
| 102 | 114 | """ | |
| 103 | 115 | # Build minimal client_metadata for the base class | |
@@ -168,6 +180,7 @@ def static_assertion_provider(token: str) -> Callable[[str], Awaitable[str]]: | |||
| 168 | 180 | storage=my_token_storage, | |
| 169 | 181 | client_id="my-client-id", | |
| 170 | 182 | assertion_provider=static_assertion_provider(my_prebuilt_jwt), | |
| 183 | + issuer="https://auth.example.com", | ||
| 171 | 184 | ) | |
| 172 | 185 | ``` | |
| 173 | 186 | ||
@@ -202,6 +215,7 @@ class SignedJWTParameters(BaseModel): | |||
| 202 | 215 | storage=my_token_storage, | |
| 203 | 216 | client_id="my-client-id", | |
| 204 | 217 | assertion_provider=jwt_params.create_assertion_provider(), | |
| 218 | + issuer="https://auth.example.com", | ||
| 205 | 219 | ) | |
| 206 | 220 | ``` | |
| 207 | 221 | """ | |
@@ -267,6 +281,7 @@ async def get_workload_identity_token(audience: str) -> str: | |||
| 267 | 281 | storage=my_token_storage, | |
| 268 | 282 | client_id="my-client-id", | |
| 269 | 283 | assertion_provider=get_workload_identity_token, | |
| 284 | + issuer="https://auth.example.com", | ||
| 270 | 285 | ) | |
| 271 | 286 | ``` | |
| 272 | 287 | ||
@@ -280,6 +295,7 @@ async def get_workload_identity_token(audience: str) -> str: | |||
| 280 | 295 | storage=my_token_storage, | |
| 281 | 296 | client_id="my-client-id", | |
| 282 | 297 | assertion_provider=static_assertion_provider(my_prebuilt_jwt), | |
| 298 | + issuer="https://auth.example.com", | ||
| 283 | 299 | ) | |
| 284 | 300 | ``` | |
| 285 | 301 | ||
@@ -298,6 +314,7 @@ async def get_workload_identity_token(audience: str) -> str: | |||
| 298 | 314 | storage=my_token_storage, | |
| 299 | 315 | client_id="my-client-id", | |
| 300 | 316 | assertion_provider=jwt_params.create_assertion_provider(), | |
| 317 | + issuer="https://auth.example.com", | ||
| 301 | 318 | ) | |
| 302 | 319 | ``` | |
| 303 | 320 | """ | |
@@ -327,7 +344,8 @@ def __init__( | |||
| 327 | 344 | registered with. When set, an assertion is only minted, and token requests | |
| 328 | 345 | are only built, once authorization server metadata whose `issuer` is exactly this | |
| 329 | 346 | string has been discovered; otherwise the flow stops with `OAuthFlowError`. | |
| 330 | - When omitted, whichever authorization server discovery yields is used. | ||
| 347 | + Omitting it is deprecated (`MCPDeprecationWarning`) and it will be required in | ||
| 348 | + 3.0; until then, whichever authorization server discovery yields is used. | ||
| 331 | 349 | """ | |
| 332 | 350 | # Build minimal client_metadata for the base class | |
| 333 | 351 | client_metadata = OAuthClientMetadata( | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -7,6 +7,7 @@ | |||
| 7 | 7 | from inline_snapshot import snapshot | |
| 8 | 8 | from pydantic import AnyHttpUrl | |
| 9 | 9 | ||
| 10 | + from mcp import MCPDeprecationWarning | ||
| 10 | 11 | from mcp.client.auth import OAuthClientProvider, OAuthFlowError | |
| 11 | 12 | from mcp.client.auth.extensions.client_credentials import ( | |
| 12 | 13 | ClientCredentialsOAuthProvider, | |
@@ -57,6 +58,7 @@ async def test_init_sets_client_info(self, mock_storage: MockTokenStorage): | |||
| 57 | 58 | storage=mock_storage, | |
| 58 | 59 | client_id="test-client-id", | |
| 59 | 60 | client_secret="test-client-secret", | |
| 61 | + issuer="https://api.example.com", | ||
| 60 | 62 | ) | |
| 61 | 63 | ||
| 62 | 64 | # client_info is set during _initialize | |
@@ -77,6 +79,7 @@ async def test_init_with_scopes(self, mock_storage: MockTokenStorage): | |||
| 77 | 79 | client_id="test-client-id", | |
| 78 | 80 | client_secret="test-client-secret", | |
| 79 | 81 | scope="read write", | |
| 82 | + issuer="https://api.example.com", | ||
| 80 | 83 | ) | |
| 81 | 84 | ||
| 82 | 85 | await provider._initialize() | |
@@ -92,6 +95,7 @@ async def test_init_with_client_secret_post(self, mock_storage: MockTokenStorage | |||
| 92 | 95 | client_id="test-client-id", | |
| 93 | 96 | client_secret="test-client-secret", | |
| 94 | 97 | token_endpoint_auth_method="client_secret_post", | |
| 98 | + issuer="https://api.example.com", | ||
| 95 | 99 | ) | |
| 96 | 100 | ||
| 97 | 101 | await provider._initialize() | |
@@ -107,6 +111,7 @@ async def test_exchange_token_client_credentials(self, mock_storage: MockTokenSt | |||
| 107 | 111 | client_id="test-client-id", | |
| 108 | 112 | client_secret="test-client-secret", | |
| 109 | 113 | scope="read write", | |
| 114 | + issuer="https://api.example.com", | ||
| 110 | 115 | ) | |
| 111 | 116 | provider.context.oauth_metadata = OAuthMetadata( | |
| 112 | 117 | issuer=AnyHttpUrl("https://api.example.com"), | |
@@ -135,6 +140,7 @@ async def test_exchange_token_client_secret_post_includes_client_id(self, mock_s | |||
| 135 | 140 | client_secret="test-client-secret", | |
| 136 | 141 | token_endpoint_auth_method="client_secret_post", | |
| 137 | 142 | scope="read write", | |
| 143 | + issuer="https://api.example.com", | ||
| 138 | 144 | ) | |
| 139 | 145 | await provider._initialize() | |
| 140 | 146 | provider.context.oauth_metadata = OAuthMetadata( | |
@@ -161,6 +167,7 @@ async def test_exchange_token_without_scopes(self, mock_storage: MockTokenStorag | |||
| 161 | 167 | storage=mock_storage, | |
| 162 | 168 | client_id="test-client-id", | |
| 163 | 169 | client_secret="test-client-secret", | |
| 170 | + issuer="https://api.example.com", | ||
| 164 | 171 | ) | |
| 165 | 172 | provider.context.oauth_metadata = OAuthMetadata( | |
| 166 | 173 | issuer=AnyHttpUrl("https://api.example.com"), | |
@@ -192,6 +199,7 @@ async def mock_assertion_provider(audience: str) -> str: # pragma: no cover | |||
| 192 | 199 | storage=mock_storage, | |
| 193 | 200 | client_id="test-client-id", | |
| 194 | 201 | assertion_provider=mock_assertion_provider, | |
| 202 | + issuer="https://api.example.com", | ||
| 195 | 203 | ) | |
| 196 | 204 | ||
| 197 | 205 | # client_info is set during _initialize | |
@@ -215,6 +223,7 @@ async def mock_assertion_provider(audience: str) -> str: | |||
| 215 | 223 | client_id="test-client-id", | |
| 216 | 224 | assertion_provider=mock_assertion_provider, | |
| 217 | 225 | scope="read write", | |
| 226 | + issuer="https://auth.example.com", | ||
| 218 | 227 | ) | |
| 219 | 228 | provider.context.oauth_metadata = OAuthMetadata( | |
| 220 | 229 | issuer=AnyHttpUrl("https://auth.example.com"), | |
@@ -246,6 +255,7 @@ async def mock_assertion_provider(audience: str) -> str: | |||
| 246 | 255 | storage=mock_storage, | |
| 247 | 256 | client_id="test-client-id", | |
| 248 | 257 | assertion_provider=mock_assertion_provider, | |
| 258 | + issuer="https://auth.example.com", | ||
| 249 | 259 | ) | |
| 250 | 260 | provider.context.oauth_metadata = OAuthMetadata( | |
| 251 | 261 | issuer=AnyHttpUrl("https://auth.example.com"), | |
@@ -436,6 +446,65 @@ async def test_provider_picks_its_configured_issuer_among_several_advertised_ser | |||
| 436 | 446 | await flow.aclose() | |
| 437 | 447 | ||
| 438 | 448 | ||
| 449 | + @pytest.mark.parametrize("kind", ["secret", "jwt"]) | ||
| 450 | + def test_constructing_without_issuer_is_deprecated(mock_storage: MockTokenStorage, kind: str) -> None: | ||
| 451 | + """SDK-defined: leaving `issuer` out is allowed, and the provider says at construction that | ||
| 452 | + token requests will follow whichever authorization server the MCP server advertises.""" | ||
| 453 | + | ||
| 454 | + async def assertion_provider(audience: str) -> str: | ||
| 455 | + raise NotImplementedError | ||
| 456 | + | ||
| 457 | + with pytest.warns(MCPDeprecationWarning) as recorded: | ||
| 458 | + if kind == "secret": | ||
| 459 | + ClientCredentialsOAuthProvider( | ||
| 460 | + server_url=_SERVER_URL, storage=mock_storage, client_id="c", client_secret="s" | ||
| 461 | + ) | ||
| 462 | + else: | ||
| 463 | + PrivateKeyJWTOAuthProvider( | ||
| 464 | + server_url=_SERVER_URL, storage=mock_storage, client_id="c", assertion_provider=assertion_provider | ||
| 465 | + ) | ||
| 466 | + | ||
| 467 | + [warning] = recorded | ||
| 468 | + assert warning.filename == __file__ | ||
| 469 | + assert str(warning.message) == ( | ||
| 470 | + "Omitting `issuer` is deprecated and it will be required in 3.0. Without it, the MCP server " | ||
| 471 | + "decides which authorization server receives this client's credentials; pass " | ||
| 472 | + "issuer=<your authorization server's issuer URL> so they are only ever sent there." | ||
| 473 | + ) | ||
| 474 | + | ||
| 475 | + | ||
| 476 | + @pytest.mark.anyio | ||
| 477 | + @pytest.mark.parametrize("kind", ["secret", "jwt"]) | ||
| 478 | + async def test_without_issuer_the_exchange_follows_whichever_server_was_discovered( | ||
| 479 | + mock_storage: MockTokenStorage, kind: str | ||
| 480 | + ) -> None: | ||
| 481 | + """SDK-defined: with no `issuer` configured the token request is built from whatever metadata | ||
| 482 | + discovery produced, as before.""" | ||
| 483 | + | ||
| 484 | + async def assertion_provider(audience: str) -> str: | ||
| 485 | + return "jwt" | ||
| 486 | + | ||
| 487 | + with pytest.warns(MCPDeprecationWarning, match="Omitting `issuer` is deprecated"): | ||
| 488 | + if kind == "secret": | ||
| 489 | + provider: OAuthClientProvider = ClientCredentialsOAuthProvider( | ||
| 490 | + server_url=_SERVER_URL, storage=mock_storage, client_id="c", client_secret="s" | ||
| 491 | + ) | ||
| 492 | + else: | ||
| 493 | + provider = PrivateKeyJWTOAuthProvider( | ||
| 494 | + server_url=_SERVER_URL, storage=mock_storage, client_id="c", assertion_provider=assertion_provider | ||
| 495 | + ) | ||
| 496 | + flow = provider.async_auth_flow(httpx2.Request("POST", _SERVER_URL)) | ||
| 497 | + | ||
| 498 | + token_request = await _answer_discovery( | ||
| 499 | + flow, | ||
| 500 | + authorization_server="https://elsewhere.example.com", | ||
| 501 | + metadata=_metadata_for("https://elsewhere.example.com"), | ||
| 502 | + ) | ||
| 503 | + | ||
| 504 | + assert (token_request.method, str(token_request.url)) == ("POST", "https://elsewhere.example.com/token") | ||
| 505 | + await flow.aclose() | ||
| 506 | + | ||
| 507 | + | ||
| 439 | 508 | def test_an_issuer_that_is_not_an_http_url_is_rejected_at_construction(mock_storage: MockTokenStorage) -> None: | |
| 440 | 509 | """SDK-defined: `issuer=` is the authorization server's issuer URL; anything else is a configuration | |
| 441 | 510 | error on both machine-to-machine providers.""" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -105,6 +105,7 @@ async def test_the_one_more_provider_is_private_key_jwt() -> None: | |||
| 105 | 105 | storage=tutorial002.InMemoryTokenStorage(), | |
| 106 | 106 | client_id="reporting-agent", | |
| 107 | 107 | assertion_provider=static_assertion_provider("a.prebuilt.jwt"), | |
| 108 | + issuer="http://localhost:9000", | ||
| 108 | 109 | ) | |
| 109 | 110 | assert isinstance(provider, OAuthClientProvider) | |
| 110 | 111 | assert isinstance(provider, httpx2.Auth) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -373,6 +373,7 @@ async def test_client_credentials_provider_obtains_a_token_without_an_authorize_ | |||
| 373 | 373 | client_id="m2m-client", | |
| 374 | 374 | client_secret="m2m-secret", | |
| 375 | 375 | scope="mcp", | |
| 376 | + issuer=BASE_URL, | ||
| 376 | 377 | ) | |
| 377 | 378 | ||
| 378 | 379 | with anyio.fail_after(5): | |
@@ -424,6 +425,7 @@ async def assertion_provider(audience: str) -> str: | |||
| 424 | 425 | client_id="m2m-jwt-client", | |
| 425 | 426 | assertion_provider=assertion_provider, | |
| 426 | 427 | scope="mcp", | |
| 428 | + issuer=BASE_URL, | ||
| 427 | 429 | ) | |
| 428 | 430 | ||
| 429 | 431 | with anyio.fail_after(5): | |
| Back | FazBrowse Home | New Git URL |
0 commit comments