It started with a SalesForce API returning 400 errors. It ended with a PR to Anthropic's MCP Python SDK. Here's the story.
The Error
I was building an MCP server that needed SalesForce access. The OAuth flow worked—I got redirected, approved permissions, received an auth code. Then the token exchange failed.
400 Bad Request: unauthorized_client
The error was maddeningly vague. Unauthorized how? The client ID was correct. The redirect URI matched. Everything looked right.
First Instinct: My Code Is Wrong
I spent an hour checking my configuration. Client ID, client secret, redirect URI, grant type. Compared against SalesForce docs. Matched perfectly.
Added logging everywhere. The request being sent looked correct. The scope parameter was...wait.
The scope being sent wasn't what I configured. I'd set api refresh_token (the minimum SalesForce needs). The request showed openid email profile api refresh_token offline_access.
Where was that coming from?
Down the Stack
I wasn't setting those scopes. So something else was. Time to trace the request.
The MCP SDK has an OAuth client provider that handles the auth flow. I found it in src/mcp/client/auth/oauth2.py. Big file. Lots of state machines.
I searched for "scope". Found this in async_auth_flow:
self.context.client_metadata.scope = get_client_metadata_scopes(
extract_scope_from_www_auth(response),
self.context.protected_resource_metadata,
self.context.oauth_metadata,
)No condition. No check if scope was already set. Just an assignment.
That meant: even if you explicitly set a scope in your client metadata, the SDK would overwrite it with whatever it computed from server discovery.
The Fix
Five lines:
if self.context.client_metadata.scope is None:
self.context.client_metadata.scope = get_client_metadata_scopes(
extract_scope_from_www_auth(response),
self.context.protected_resource_metadata,
self.context.oauth_metadata,
)Only compute scopes if the client didn't provide them explicitly.
What I Learned
Error messages lie. "Unauthorized client" had nothing to do with the client being unauthorized. The scope was wrong, and SalesForce returns generic errors.
Trace the data, not the logic. I wasted time reading code trying to understand the auth flow. What I should have done first: log the actual request being sent and compare it to what I expected.
Explicit is better. The SDK was trying to be helpful by auto-discovering scopes. But implicit behavior that overrides explicit configuration is a bug, not a feature.
Read the code. The fix took 10 minutes once I found the problem. Finding the problem took 3 hours. Most of that was me assuming my code was wrong instead of reading the SDK.
The PR
PR #2324 is open now. Small diff, detailed description, clear reproduction steps.
Maybe it gets merged. Maybe maintainers prefer a different approach. Either way, I understand OAuth flows better than I did yesterday.
That's the real win.