fix(mcp): always emit a string scope in the /token response #106
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/YT-48-token-scope-null"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Fixes YT-48: the
/tokenresponse emitted"scope": nullwhen the client requested no scope, and the Claude Code MCP client rejects that (expected string, received null). A full, server-side-successful OAuth dance (POST /token ... status=200, tokens issued) therefore left the clientnot authenticated.Root cause
token_success(oauth_as.rs) serialized"scope": scopewherescope: Option<&str>. The consent handler only defaults the scope for the consent page display; the value persisted into the authorization code is the client'sscopeparam, which isNonewhen Claude sends none. ThatNoneflows totoken_successand serializes as JSON null. The refresh grant shares the same emit point and exposure.Fix
Default a
Nonescope toADVERTISED_SCOPE(offline_access) intoken_success, so the responsescopeis always a non-null string. This is the single site both the authorization_code and refresh_token grants converge on, and matches what/.well-known/*advertises and theWWW-Authenticatechallenge reports.Chose always-emit-a-string over omitting the field: a client whose schema requires a string would fail on an absent field too; a guaranteed string satisfies both required- and optional-string schemas.
Invariant sweep (the /token response scope is always a JSON string)
oauth_as.rstoken_successADVERTISED_SCOPEtoken_code,token_refreshviaissue_token_pair)token_successit_oauth.rs/oauth_hub.rstoken-body literalsTests
full_authorization_code_and_refresh_rotation(which authorizes with no scope) to assert both the authorization_code and refresh_token responses carryscope == "offline_access"(previously null).just pre-commitgreen (fmt, clippy-D warnings, build, 88 tests).Manual end-to-end verification against the Claude Code client to follow once deployed.
The token endpoint serialized the granted scope as `"scope": scope`, which becomes JSON `null` when the client requested no scope (the authorization code is stored without one). The Claude Code MCP client validates the token response and rejects a null scope ("expected string, received null"), so an otherwise-successful OAuth dance left the client unauthenticated. Default a None scope to ADVERTISED_SCOPE ("offline_access") in token_success, the single point both the authorization_code and refresh_token grants converge on, so the response scope is always a non-null string, consistent with the discovery metadata and the WWW-Authenticate challenge. #YT-48 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>