Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c312b6f
[Server] Narrow authorization to the resource server role
chr-hertel Oct 5, 2026
26ca75f
[Server] Answer unresolvable token signing keys with 401
chr-hertel Oct 6, 2026
0c5c97e
[Server] Reject access tokens without exp
chr-hertel Oct 6, 2026
b27f0ad
[Server] Discover issuer keys lazily, one key set per algorithm
chr-hertel Oct 6, 2026
c7fdd85
[Server] Document the access token parameter BC breaks
chr-hertel Oct 6, 2026
a60193c
[Server] Dedupe scopes on insert in ScopePolicy
chr-hertel Oct 6, 2026
e4983f9
[Server] Hand on implied scopes with the access token
chr-hertel Oct 6, 2026
3e45948
[Server] Challenge other authorization schemes without error code
chr-hertel Oct 6, 2026
1979746
[Server] Explain bearer parsing and header escaping
chr-hertel Oct 6, 2026
cb6f5aa
[Server] Keep the resource query in the protected resource metadata URL
chr-hertel Oct 6, 2026
f591976
[Server] Cover SecureUrl and forbidden batch requests
chr-hertel Oct 6, 2026
f3a3769
[Docs] Document the AccessToken request attribute as integration seam
chr-hertel Oct 6, 2026
b88d13f
[Examples] Note that Entra step-up needs the prefixed scope
chr-hertel Oct 6, 2026
260caea
[Server] Require a numeric exp in access tokens
chr-hertel Oct 6, 2026
6e9a00e
[Server] Expand the scope hierarchy once per request
chr-hertel Oct 6, 2026
f2279c8
[Server] Share scope validation between policy and metadata
chr-hertel Oct 6, 2026
cd2f2e6
[Server] Back off briefly after a failed OIDC discovery
chr-hertel Oct 6, 2026
0082fb1
[Server] Note the global leeway swap in JwtTokenValidator
chr-hertel Oct 6, 2026
d617b26
[Server] Accept only https, or http on loopback hosts, in SecureUrl
chr-hertel Oct 7, 2026
f817703
[Server] Serve metadata of a queryless resource only without query
chr-hertel Oct 7, 2026
2792875
[Server] Restrict scopes to the RFC 6749 scope-token grammar
chr-hertel Oct 7, 2026
db4178c
[Server] Keep the resource path verbatim in the metadata URL
chr-hertel Oct 7, 2026
bc875e0
[Server] Resolve the signing key before swapping the JWT leeway
chr-hertel Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ All notable changes to `mcp/sdk` will be documented in this file.
* Fix OIDC discovery rejecting issuers with a trailing slash (e.g. Authentik, Auth0).
* Fix stateless SSE streams holding back frames until close when PHP output buffering is enabled.
* Reject a recognized `Mcp-Param-*` header whose mirrored argument is absent from the body with `-32020`, instead of accepting the request (SEP-2243).
* Fix `JwtTokenValidator` with several issuers always fetching the keys of the first one: keys now come from the issuer the token claims, which must be configured.
* Fix `RequestEvent`, `ResponseEvent` and `ErrorEvent` not being dispatched for `2026-07-28` requests.
* [BC Break] Validate a tool result's `structuredContent` against the tool's `outputSchema`, which the specification requires the server to honour. A mismatch is answered with a `CallToolResult` carrying `isError: true` instead of the non-conforming value, matching the TypeScript, Python and Java SDKs. Skipped when the tool declares no `outputSchema`, when the result carries no `structuredContent`, and when the result is already an error. Return `new \stdClass()` for an empty object, since `[]` is sent as an array.
* Stop the server `Protocol` from logging full JSON-RPC payloads (tool arguments, client replies) at info level: info records now carry only the method and id, the raw message is logged at debug level.
Expand All @@ -25,6 +24,14 @@ All notable changes to `mcp/sdk` will be documented in this file.
* Add `Client::getServerCapabilities()`, returning what the server declared in `initialize` or, from `2026-07-28` on, in `server/discover`.
* [BC Break] `ClientStateInterface` declares `setServerCapabilities()` and `getServerCapabilities()`, which a custom implementation has to add.
* Add a `listen` option to the client's `HttpTransport`, opening the standalone GET stream on which a 2025-era server sends requests and notifications outside of a client request, like `roots/list`. Needs a PSR-18 client that streams response bodies, such as `symfony/http-client`.
* [BC Break] Narrow authorization to the resource server role (ADR 0002): remove `OAuthProxyMiddleware`, `ClientRegistrationMiddleware`, `ClientRegistrarInterface` and `ClientRegistrationException`.
* [BC Break] Replace `OAuthRequestMetaMiddleware` with `RequestContext::getAccessToken()`, returning the validated `Server\Authorization\AccessToken`; `AuthorizationResult::allow()` takes an `AccessToken` instead of request attributes.
* [BC Break] Add an `?AccessToken $accessToken` parameter to `Protocol::processInput()`, `BaseTransport::handleMessage()` and `StreamableHttpTransport::handlePostRequest()`, and as fourth argument of the `TransportInterface::onMessage()` listener; overrides need the new signature.
* [BC Break] Make `JwtTokenValidator` final with a single issuer instead of a list of issuer aliases, and a `$keys` set (e.g. `CachedKeySet`); `JwtTokenValidator::fromIssuer()` discovers and caches keys in a PSR-6 pool, refetching on unknown key ids. The `alg` allowlist is enforced, `$tokenType` and `$leeway` are added, `requireScopes()` is removed in favour of `ScopePolicy`.
* [BC Break] Remove `JwksProvider`, `JwksProviderInterface`, `OidcDiscoveryInterface` and the OIDC metadata policies; `OidcDiscovery` is internal.
* [BC Break] `ProtectedResourceMetadata` requires `$resource`, serves at the path derived from it (RFC 9728 §3.1) and requires https except for loopback hosts; drops localized, policy, ToS, extra fields and `$metadataPaths`.
* [BC Break] Add `ScopePolicy` as third argument of `AuthorizationMiddleware`, answering `403 insufficient_scope` per method and tool, with scope hierarchies; the `resource_metadata` challenge URL comes from the configured resource instead of the `Host` header.
* Expose `WWW-Authenticate` in the default `CorsMiddleware`.

0.8.0
-----
Expand Down
7 changes: 5 additions & 2 deletions adr/0001-oauth-authorization-server-out-of-scope.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 0001 — The MCP server is an OAuth Resource Server, not an Authorization Server

- Status: Accepted
- Status: Accepted, amended by [0002](0002-resource-server-only.md)
- Date: 2026-06-15

## Context
Expand All @@ -10,7 +10,7 @@ OAuth 2.1 defines three distinct roles:
| Role | What it does | Status in this SDK | Scope |
|------|--------------|--------------------|-------|
| Resource Server | Validates incoming bearer tokens, serves Protected Resource Metadata (RFC 9728), emits `WWW-Authenticate` | Shipped (`AuthorizationMiddleware`, `JwtTokenValidator`, `ProtectedResourceMetadata`) | **IN scope** |
| Delegation / proxy to an upstream AS | Forwards `/authorize` and `/token` to your existing IdP | Shipped (`OAuthProxyMiddleware`) | **IN scope — delegation ONLY** |
| Delegation / proxy to an upstream AS | Forwards `/authorize` and `/token` to your existing IdP | Removed (`OAuthProxyMiddleware`) | **OUT of scope since [0002](0002-resource-server-only.md)** |
| Authorization Server / Identity Provider (IdP) | Mints its own tokens, registers clients, runs login and consent | Absent | **OUT of scope** |

The SDK repeatedly receives pull requests that move it toward becoming a full OAuth 2.1
Expand All @@ -32,6 +32,9 @@ intent.

## Decision

> Amended by [0002](0002-resource-server-only.md): delegation via `OAuthProxyMiddleware` and
> Dynamic Client Registration are no longer provided. The statements on them below are superseded.

**The MCP server is an OAuth 2.1 Resource Server that MAY delegate to an upstream
authorization server. It will NOT issue tokens or act as an Identity Provider.**

Expand Down
45 changes: 45 additions & 0 deletions adr/0002-resource-server-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 0002 — Resource Server only: no delegation of the OAuth flow

- Status: Accepted
- Date: 2026-10-05
- Amends: [0001](0001-oauth-authorization-server-out-of-scope.md)

## Context

[0001](0001-oauth-authorization-server-out-of-scope.md) kept delegation in scope: the
`OAuthProxyMiddleware` fronted an upstream authorization server's `/authorize` and `/token`
endpoints, and the `ClientRegistrationMiddleware` served Dynamic Client Registration (RFC 7591)
backed by a user-provided registrar. Hardening that surface showed it cannot be made safe
without becoming the authorization server 0001 rules out:

- Holding the server's client credentials, the proxy would have to decide which clients and
grants it redeems them for and enforce PKCE on their behalf — authorization server logic.
- Its metadata named the MCP server as issuer while the upstream issued the tokens. The
2026-07-28 specification makes clients validate the `iss` authorization response parameter
(RFC 9207), so compliant clients reject the proxied flow.
- It could not serve Client ID Metadata Documents, the registration mechanism the
specification prefers, since the upstream never sees those client ids.
- Forwarding dynamically registered clients under one upstream client id is the confused deputy
case, for which the specification requires per-client user consent — a consent UI.
- Dynamic Client Registration is deprecated in the 2026-07-28 specification.

None of it is needed: the Protected Resource Metadata points clients at the authorization server
directly, which is how the specification lays out the flow.

## Decision

**The SDK implements the Resource Server role only.** It validates tokens, serves Protected
Resource Metadata, emits `WWW-Authenticate` challenges and enforces scopes. It does not proxy,
front or delegate any authorization server endpoint, and it does not serve client registration.

`OAuthProxyMiddleware`, `ClientRegistrationMiddleware`, `ClientRegistrarInterface` and
`ClientRegistrationException` are removed. Pull requests reintroducing them, or any other
authorization server endpoint, are declined by reference to this ADR and 0001.

## Consequences

- The authorization surface shrinks to what the specification asks of an MCP server, all of
which can be tested against its MUSTs.
- Identity providers lacking what MCP clients need (Dynamic Client Registration or Client ID
Metadata Documents, PKCE metadata, RFC 8707 resource indicators) are bridged by a gateway or
authorization server in front of them — a deployment concern outside this SDK.
1 change: 1 addition & 0 deletions adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ a pull request that conflicts with an accepted decision.
## Records

- [0001 — The MCP server is an OAuth Resource Server, not an Authorization Server](0001-oauth-authorization-server-out-of-scope.md)
- [0002 — Resource Server only: no delegation of the OAuth flow](0002-resource-server-only.md)
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0"
},
"suggest": {
"firebase/php-jwt": "Required for the JwtTokenValidator.",
"psr/cache-implementation": "Required for JwtTokenValidator::fromIssuer() to cache metadata and keys.",
"symfony/finder": "Required for file-based discovery."
},
"require-dev": {
Expand All @@ -50,6 +52,7 @@
"phpdocumentor/shim": "^3",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.5",
"psr/cache": "^1.0 || ^2.0 || ^3.0",
"psr/simple-cache": "^2.0 || ^3.0",
"symfony/cache": "^5.4 || ^6.4 || ^7.3 || ^8.0",
"symfony/console": "^5.4 || ^6.4 || ^7.3 || ^8.0",
Expand Down
Loading
Loading