BYO OAuth: oauth.clientSecret in .mcp.json is silently ignored; clientSecretHelper never executed
Environment
- Claude Code 2.1.247
- Windows 11 (10.0.26100)
- MCP server: HTTP transport,
oauth.mode: "byo", authorization server is an Amazon Cognito user pool with a hosted UI domain
Summary
When an HTTP MCP server is configured in .mcp.json with oauth.mode: "byo",
the oauth.clientSecret value is never read. Token exchange is performed without a
client secret and the IdP rejects it. No error or warning is emitted at any point —
the config is reported as valid and the only symptom is an authentication failure that
appears to blame the secret.
Two other plausible ways to supply the secret also fail silently.
Expected
Either the configured value is used, or the config loader warns that it is being ignored.
Actual
claude mcp login <name> completes the browser authorization, then fails:
Couldn't complete authentication for "<name>": invalid_client_secret
The same client_id / client_secret pair posted directly to the IdP token endpoint
with a dummy code returns {"error":"invalid_grant"} — i.e. client authentication
succeeds and only the code is rejected. Omitting the secret from that same request
reproduces the exact error Claude Code surfaces. So the secret is valid and simply is
not being sent.
Root cause
In the shipped binary, clientInformation() resolves the two fields from different sources:
n = e?.mcpOAuthClientConfig?.[t]?.clientSecret, // secret: credential store only
r = this.serverConfig.oauth?.clientId, // clientId: config file
clientSecret is read exclusively from credentialStore.mcpOAuthClientConfig[key] and
never from serverConfig.oauth. Nothing warns when a config-supplied secret is discarded.
Related silent failures
1. oauth.clientSecretHelper is accepted but never executed.
A helper script that writes a marker file and prints the secret was configured via.mcp.json. After a full browser login, the marker file was never created and auth
failed with invalid_client_secret. The same helper printed the correct secret when
run standalone. The path is also not validated — a nonexistent path and a relative path
both load without warning.
2. ${VAR} expansion does not apply under oauth.
An unset environment variable produces a warning in headers but not in oauth:
| Field | Unset ${VAR} |
|---|---|
| headers | [Warning] Missing environment variables: UNSET_VAR_AAA |
| oauth.clientSecret | no warning; literal string passed through |
| oauth.clientId | no warning; literal string passed through |
Workaround
Register the secret into the credential store instead of the config file:
claude mcp add --transport http <name> <url> \
--client-id <id> --client-secret --callback-port <port> -s local
(MCP_CLIENT_SECRET can be set to avoid the prompt.)
After this, claude mcp get <name> reports client_secret configured in addition toclient_id configured. That string is the only observable indication that a secret is
actually available — its absence is the sole clue when authentication fails.
Steps to reproduce
- Create
.mcp.jsonwith an http server and:
oauth: {mode: "byo", clientId, clientSecret, authorizationServer: ["<issuer>"], callbackHost: "localhost", callbackPort: <port>}
- Run
claude mcp login <name>and complete the browser login. - Authentication fails with
invalid_client_secret. - Run the workaround command above, then
claude mcp login <name>— succeeds.
Smaller issues observed while debugging
claude mcp add-jsonrejects a config containingclientSecretHelperwith
Invalid configuration: : Invalid input, while the identical config using
clientSecret is accepted.
claude mcp remove <name>deletes the stored client secret and access token in
addition to the config entry. This is not mentioned in --help and silently undoes
the workaround above.
claude mcp list/claude mcp getreportPending approvalfor.mcp.jsonservers
that are connected and working in an active session, so the reported status cannot be
used for diagnosis.
claude mcp logout <name>does not help withinvalid_client_secret, because the
secret lives outside the token store — an easy false lead.
- Every CLI read emits:
[mcp-sdk] SEP-2352: stored OAuth credential has no 'issuer' stamp
Impact
Because the failure mode is a silent ignore, the surfaced error (invalid_client_secret)
points at the one thing that is actually correct. Diagnosing this required reading the
shipped binary. A warning at config load — "oauth.clientSecret is ignored; useclaude mcp add --client-secret" — would remove the entire investigation.