Repository navigation
Conversation
… §2.3.1) RFC 6749 §2.3.1 requires the client identifier and the client secret to be form-urlencoded (Appendix B) before they are joined with ":" and base64-encoded into the Authorization header; a conformant token endpoint splits the decoded value on the first ":" and form-decodes both halves. Sending them raw meant a secret containing "+" arrived as a space, "%" broke the decoding, a ":" inside the client identifier moved the split, and any character outside Latin-1 made btoa throw before the request was sent. Credentials made only of unreserved characters are byte-identical on the wire.
🦋 Changeset detectedLatest commit: b4837e8 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
koriyoshi2041
left a comment
There was a problem hiding this comment.
Validated exact head b4837e8a against the RFC 6749 §2.3.1 boundary. The focused auth matrix passes 318/318, client typecheck and formatting pass, and reverting only the encoding call makes all four new regressions fail (reserved + becomes a space, raw delimiters are ambiguous, and non-Latin-1 credentials throw before fetch).
The implementation also preserves the existing wire bytes for unreserved ASCII credentials while making both halves independently form-decodable. I found no correctness issue in the changed path.
Summary
applyBasicAuthbase64-encoded the client id and secret as they were. RFC 6749 §2.3.1 requires each half to be form-urlencoded (Appendix B) before they are joined with:, and a conformant token endpoint splits the decoded value on the first:and form-decodes both halves. Credentials containing reserved characters were therefore sent as different credentials, andbtoathrew outright for anything outside Latin-1, aborting the token request before it left the process:cid/a+ba ba+bclient id/sec:ret+with=specialclient, passwordid:sec:ret with=specialcid/p€ssInvalidCharacterErrorfrombtoa, no request sentp€ssCredentials made only of unreserved characters are byte-identical on the wire, so nothing that worked changes.
Changes
packages/client/src/client/auth.ts: a smallformUrlEncodehelper — Appendix B is theapplication/x-www-form-urlencodedserializer, which is whatURLSearchParamsimplements, includingspace → +and~ → %7E— applied to both halves.packages/client/test/client/authBasicEncoding.test.ts: three tests assert what a conformant server recovers; a fourth drives a realexchangeAuthorizationagainst a recordingfetch, so the header on the token request itself is covered..changeset/client-secret-basic-form-encoding.md.Verification
npx vitest run test/client/authBasicEncoding.test.ts— 4 passed.main— 4 failed, e.g.expected { username: 'cid', password: 'a b' } to deeply equal { username: 'cid', password: 'a+b' }andInvalidCharacterErrorwhere the test asserts it does not throw.vitest run test/client/auth.test.ts test/client/authExtensions.test.ts— 314 passed.packages/clientsuite — 990 passed, 12 skipped. The only failing files are the threespawn pnpm ENOENTbuild-dependent ones, which fail identically without this change.pnpm --filter @modelcontextprotocol/client run typecheckclean,eslint src/client/auth.tsclean,prettier --checkclean on the changed files.Notes
v1.xcarries the same code (src/client/auth.ts:358). Happy to send the equivalent change to that branch too if you'd like it there.AI disclosure
Per the organization's AI contribution policy: this PR was prepared with Claude Code (an AI coding assistant) under my direction — it located the encoding deviation and drafted the code and tests. I reviewed the change and the evidence above, the reproduction, tests and suite runs were executed locally in my environment, and I can explain why the change is correct: §2.3.1 is explicit that both halves are encoded, and the tests check what a conformant server recovers rather than only the string we emit.