From dd489aa12018da285f362d7696728345bd0dfa47 Mon Sep 17 00:00:00 2001 From: Tyler Gibbs Date: Wed, 22 Jul 2026 10:54:14 -0500 Subject: [PATCH] fix(client): accept form-encoded token responses --- .changeset/oauth-token-form-responses.md | 5 +++++ packages/client/src/client/auth.ts | 6 +++++- packages/client/test/client/auth.test.ts | 27 ++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .changeset/oauth-token-form-responses.md diff --git a/.changeset/oauth-token-form-responses.md b/.changeset/oauth-token-form-responses.md new file mode 100644 index 0000000000..37b3ce2b8e --- /dev/null +++ b/.changeset/oauth-token-form-responses.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/client': patch +--- + +Accept OAuth token responses encoded as `application/x-www-form-urlencoded` in addition to JSON. diff --git a/packages/client/src/client/auth.ts b/packages/client/src/client/auth.ts index 9ebc6fd251..f5484539f6 100644 --- a/packages/client/src/client/auth.ts +++ b/packages/client/src/client/auth.ts @@ -2097,7 +2097,11 @@ export async function executeTokenRequest( throw await parseErrorResponse(response); } - const json: unknown = await response.json(); + const contentType = response.headers?.get?.('content-type')?.split(';', 1)[0]?.trim().toLowerCase(); + const json: unknown = + contentType === 'application/x-www-form-urlencoded' + ? Object.fromEntries(new URLSearchParams(await response.text())) + : await response.json(); try { return OAuthTokensSchema.parse(json); diff --git a/packages/client/test/client/auth.test.ts b/packages/client/test/client/auth.test.ts index 62c6faed9a..1c99c13d33 100644 --- a/packages/client/test/client/auth.test.ts +++ b/packages/client/test/client/auth.test.ts @@ -2005,6 +2005,33 @@ describe('OAuth Authorization', () => { expect(body.get('redirect_uri')).toBe('http://localhost:3000/callback'); expect(body.get('resource')).toBe('https://api.example.com/mcp-server'); }); + + it('accepts form-encoded token responses', async () => { + mockFetch.mockResolvedValueOnce( + new Response( + new URLSearchParams({ + access_token: validTokens.access_token, + token_type: validTokens.token_type, + expires_in: String(validTokens.expires_in), + refresh_token: validTokens.refresh_token! + }), + { + status: 200, + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } + } + ) + ); + + const tokens = await exchangeAuthorization('https://auth.example.com', { + clientInformation: validClientInfo, + authorizationCode: 'code123', + codeVerifier: 'verifier123', + redirectUri: 'http://localhost:3000/callback' + }); + + expect(tokens).toEqual(validTokens); + }); + it('exchanges code for tokens with auth', async () => { mockFetch.mockResolvedValueOnce({ ok: true,