From fa780fa85be93718f16a5f710a64bfb2ce7ec89b Mon Sep 17 00:00:00 2001 From: lanxevo3 Date: Thu, 26 Mar 2026 16:55:19 -0500 Subject: [PATCH 1/2] fix(auth): enforce form-urlencoded Content-Type for token endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OAuth 2.1 §3.2 requires token endpoint requests to use application/x-www-form-urlencoded regardless of grant type. Add an explicit header.set() call immediately before the fetch in executeTokenRequest() to prevent any addClientAuthentication implementation from accidentally overriding the Content-Type. Fixes modelcontextprotocol/inspector#1160 --- packages/client/src/client/auth.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/client/src/client/auth.ts b/packages/client/src/client/auth.ts index 1a021be18..0391e35e9 100644 --- a/packages/client/src/client/auth.ts +++ b/packages/client/src/client/auth.ts @@ -1439,6 +1439,10 @@ export async function executeTokenRequest( applyClientAuthentication(authMethod, clientInformation as OAuthClientInformation, headers, tokenRequestParams); } + // Ensure Content-Type is always form-urlencoded for the token endpoint (OAuth 2.1 §3.2). + // Some addClientAuthentication implementations may have inadvertently set a different value. + headers.set('Content-Type', 'application/x-www-form-urlencoded'); + const response = await (fetchFn ?? fetch)(tokenUrl, { method: 'POST', headers, From d16f219383c2c416e7671392b1c17547d76c2205 Mon Sep 17 00:00:00 2001 From: lanxevo3 Date: Sat, 28 Mar 2026 14:40:49 -0500 Subject: [PATCH 2/2] fix(auth): guard against silent subpath loss when AS metadata discovery fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When discoverAuthorizationServerMetadata() returns undefined, the SDK falls back to constructing /authorize, /token, or /register from the authorizationServerUrl. new URL('/authorize', 'https://example.com/admin') silently resolves to https://example.com/authorize — the '/admin' subpath is completely lost. This affects servers with non-root AS URLs (e.g. PRM returns https://example.com/admin but the actual endpoints are at /admin/oauth/authorize). The fix adds a guard at all three fallback locations: if pathname !== '/', throw an informative error explaining the discovery failure and pointing to the correct metadata endpoint, instead of silently producing a broken URL. Fixes #1716. --- packages/client/src/client/auth.ts | 35 +++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/client/src/client/auth.ts b/packages/client/src/client/auth.ts index 0391e35e9..6302ab282 100644 --- a/packages/client/src/client/auth.ts +++ b/packages/client/src/client/auth.ts @@ -1338,6 +1338,17 @@ export async function startAuthorization( throw new Error(`Incompatible auth server: does not support code challenge method ${AUTHORIZATION_CODE_CHALLENGE_METHOD}`); } } else { + // Guard against silent path loss: new URL('/authorize', 'https://example.com/admin') + // → https://example.com/authorize (subpath '/admin' is silently lost). + // Only use root-path fallback when AS is at the domain root. + if (authorizationServerUrl.pathname !== '/') { + throw new Error( + `Authorization server metadata discovery failed for '${authorizationServerUrl.href}'. ` + + `Cannot safely construct '/authorize' — the server URL has a non-root path. ` + + `Ensure the AS metadata endpoint is reachable at '${authorizationServerUrl.origin}/.well-known/oauth-authorization-server${authorizationServerUrl.pathname}' ` + + `or provide metadata explicitly.` + ); + } authorizationUrl = new URL('/authorize', authorizationServerUrl); } @@ -1420,7 +1431,20 @@ export async function executeTokenRequest( fetchFn?: FetchLike; } ): Promise { - const tokenUrl = metadata?.token_endpoint ? new URL(metadata.token_endpoint) : new URL('/token', authorizationServerUrl); + const tokenUrl = metadata?.token_endpoint + ? new URL(metadata.token_endpoint) + : (() => { + // Guard: same silent-path-loss problem as /authorize above + if (authorizationServerUrl.pathname !== '/') { + throw new Error( + `Token endpoint discovery failed for '${authorizationServerUrl.href}'. ` + + `Cannot safely construct '/token' — the server URL has a non-root path. ` + + `Ensure the AS metadata endpoint is reachable at '${authorizationServerUrl.origin}/.well-known/oauth-authorization-server${authorizationServerUrl.pathname}' ` + + `or provide metadata explicitly.` + ); + } + return new URL('/token', authorizationServerUrl); + })(); const headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded', @@ -1671,6 +1695,15 @@ export async function registerClient( registrationUrl = new URL(metadata.registration_endpoint); } else { + // Guard: same silent-path-loss problem as /authorize above + if (authorizationServerUrl.pathname !== '/') { + throw new Error( + `Dynamic client registration failed for '${authorizationServerUrl.href}'. ` + + `Cannot safely construct '/register' — the server URL has a non-root path. ` + + `Ensure the AS metadata endpoint is reachable at '${authorizationServerUrl.origin}/.well-known/oauth-authorization-server${authorizationServerUrl.pathname}' ` + + `or provide metadata explicitly.` + ); + } registrationUrl = new URL('/register', authorizationServerUrl); }