Skip to content

Commit 04dafcb

Browse files
committed
Deprecate constructing the pre-provisioned OAuth clients without an issuer
The warning becomes an MCPDeprecationWarning: omitting `issuer=` on ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider keeps working in 2.x and will be required in 3.0. The message says why (without it the MCP server decides which authorization server receives the credentials) and what to pass. docs/deprecated.md lists it next to the other SDK-level deprecation, and the OAuth clients page points there.
1 parent 91d18b4 commit 04dafcb

4 files changed

Lines changed: 22 additions & 17 deletions

File tree

docs/client/oauth-clients.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ A nightly job, a CI step, another service. There is no browser and nobody to cli
112112
What changed:
113113

114114
* 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, and says so with a `UserWarning` when it is constructed.
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.
116116
* `scope` is a space-separated string, the OAuth wire format.
117117
* Everything downstream is identical: the same `TokenStorage`, the same `httpx2.AsyncClient(auth=...)`, the same `streamable_http_client`.
118118

docs/deprecated.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Deprecated features
22

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**. Two SDK-level deprecations stand on their own account and are listed [at the end](#deprecated-sdk-helpers).
44

55
The table below names each deprecated feature, why it is going away, and the replacement to build on.
66

@@ -131,11 +131,12 @@ That is the whole API. There is no per-method switch, and you don't want one: th
131131

132132
## Deprecated SDK helpers
133133

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.
135135

136136
| Deprecated | What you do instead |
137137
|---|---|
138138
| `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. |
139140

140141
## Recap
141142

@@ -144,7 +145,7 @@ These are not spec changes, only SDK internals with a better replacement. They w
144145
* 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).
145146
* 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.
146147
* `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+
* Two SDK-level deprecations ride along: `FuncMetadata.call_fn_with_arg_validation()` is removed in 3.0, and constructing `ClientCredentialsOAuthProvider` / `PrivateKeyJWTOAuthProvider` without `issuer=` stops being allowed in 3.0.
148149
* New code should not be built on any of these.
149150

150151
Every other page in these docs teaches the current API.

src/mcp/client/auth/extensions/client_credentials.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
from mcp.client.auth.oauth2 import OAuthContext
2222
from mcp.client.auth.utils import issuers_match
2323
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata
24+
from mcp.shared.exceptions import MCPDeprecationWarning
2425

2526

2627
def _checked_issuer(issuer: str | None) -> str | None:
2728
if issuer is None:
2829
warnings.warn(
29-
"No `issuer` given: client credentials will be sent to whichever authorization server the MCP "
30-
"server advertises. Pass issuer=<your authorization server's issuer URL> to send them only there.",
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,
3134
stacklevel=3,
3235
)
3336
return None
@@ -105,8 +108,9 @@ def __init__(
105108
issuer: The issuer identifier of the authorization server that issued
106109
`client_id` and `client_secret`. When set, token requests are only built from
107110
discovered authorization server metadata whose `issuer` is exactly this string;
108-
otherwise the flow stops with `OAuthFlowError`. When omitted, whichever
109-
authorization server discovery yields is used, and a `UserWarning` says so.
111+
otherwise the flow stops with `OAuthFlowError`. Omitting it is deprecated
112+
(`MCPDeprecationWarning`) and it will be required in 3.0; until then, whichever
113+
authorization server discovery yields is used.
110114
"""
111115
# Build minimal client_metadata for the base class
112116
client_metadata = OAuthClientMetadata(
@@ -335,8 +339,8 @@ def __init__(
335339
registered with. When set, an assertion is only minted, and token requests
336340
are only built, once authorization server metadata whose `issuer` is exactly this
337341
string has been discovered; otherwise the flow stops with `OAuthFlowError`.
338-
When omitted, whichever authorization server discovery yields is used, and a
339-
`UserWarning` says so.
342+
Omitting it is deprecated (`MCPDeprecationWarning`) and it will be required in
343+
3.0; until then, whichever authorization server discovery yields is used.
340344
"""
341345
# Build minimal client_metadata for the base class
342346
client_metadata = OAuthClientMetadata(

tests/client/auth/extensions/test_client_credentials.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from inline_snapshot import snapshot
88
from pydantic import AnyHttpUrl
99

10+
from mcp import MCPDeprecationWarning
1011
from mcp.client.auth import OAuthClientProvider, OAuthFlowError
1112
from mcp.client.auth.extensions.client_credentials import (
1213
ClientCredentialsOAuthProvider,
@@ -446,16 +447,14 @@ async def test_provider_picks_its_configured_issuer_among_several_advertised_ser
446447

447448

448449
@pytest.mark.parametrize("kind", ["secret", "jwt"])
449-
def test_constructing_without_issuer_warns_where_the_credentials_will_go(
450-
mock_storage: MockTokenStorage, kind: str
451-
) -> None:
450+
def test_constructing_without_issuer_is_deprecated(mock_storage: MockTokenStorage, kind: str) -> None:
452451
"""SDK-defined: leaving `issuer` out is allowed, and the provider says at construction that
453452
token requests will follow whichever authorization server the MCP server advertises."""
454453

455454
async def assertion_provider(audience: str) -> str:
456455
raise NotImplementedError
457456

458-
with pytest.warns(UserWarning) as recorded:
457+
with pytest.warns(MCPDeprecationWarning) as recorded:
459458
if kind == "secret":
460459
ClientCredentialsOAuthProvider(
461460
server_url=_SERVER_URL, storage=mock_storage, client_id="c", client_secret="s"
@@ -468,8 +467,9 @@ async def assertion_provider(audience: str) -> str:
468467
[warning] = recorded
469468
assert warning.filename == __file__
470469
assert str(warning.message) == (
471-
"No `issuer` given: client credentials will be sent to whichever authorization server the MCP "
472-
"server advertises. Pass issuer=<your authorization server's issuer URL> to send them only there."
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."
473473
)
474474

475475

@@ -484,7 +484,7 @@ async def test_without_issuer_the_exchange_follows_whichever_server_was_discover
484484
async def assertion_provider(audience: str) -> str:
485485
return "jwt"
486486

487-
with pytest.warns(UserWarning, match="No `issuer` given"):
487+
with pytest.warns(MCPDeprecationWarning, match="Omitting `issuer` is deprecated"):
488488
if kind == "secret":
489489
provider: OAuthClientProvider = ClientCredentialsOAuthProvider(
490490
server_url=_SERVER_URL, storage=mock_storage, client_id="c", client_secret="s"

0 commit comments

Comments
 (0)