MCP store: hardcoded DCR client_name breaks OAuth against strict providers (e.g. Calendly), and the provider's error is swallowed
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 39.9k
- Forks
- 3.4k
- Avg merge
- 6h 51m
- Merged PRs (30d)
- 232
Description
Summary
MCP store OAuth connections fail against any authorization server that validates client_name with a restrictive character set. We hardcode a DCR client_name containing parentheses, which some providers reject outright. Calendly (https://mcp.calendly.com) is a confirmed case: Dynamic Client Registration is supported, but our registration request is rejected before the user ever reaches an authorize screen.
The user-visible result is a bare OAuth registration failed. toast with no indication of what went wrong or that it is our request that is malformed.
Root cause
products/mcp_store/backend/oauth.py:327 sends a fixed client name:
payload: dict[str, object] = {
"client_name": "MCP Store (PostHog)",
...
}
Calendly's registration endpoint enforces alphanumeric characters, hyphens, and spaces on that field and returns:
POST https://calendly.com/oauth/register → 400
{"error":"invalid_client_metadata",
"errors":{"name":["may only contain alphanumeric characters, hyphens, and spaces."]}}
RFC 7591 puts no charset constraint on client_name, so the provider is within its rights to be stricter than the spec — the name simply needs to be conservative. Parentheses are the only offending characters in our current value.
Verified directly against the live endpoint, identical payload, only the name changed:
client_name |
Result |
|---|---|
MCP Store (PostHog) |
400 invalid_client_metadata |
PostHog MCP Store |
201 Created, valid client_id, scopes mcp:scheduling:read / mcp:scheduling:write |
Why the error is opaque
The provider's rejection body is logged but never reaches the user.
register_dcr_clientlogsDCR registration request rejectedwithstatusandbody(oauth.py:339-345), then callsresp.raise_for_status()._register_dcr_client_or_raise(presentation/views.py:968-981) catches the resultingHTTPErrorunder a bareexcept Exception, logsDCR registration failed, and raisesDCRRegistrationFailedError.DCRRegistrationFailedErrorcarries no message (views.py:105-110), so the handler in_authorize_for_custom/install_templateresponds with a fixed{"detail": "OAuth registration failed."}.- The frontend renders
e.detailverbatim (products/mcp_store/frontend/scene/AddCustomServerForm.tsx:560).
Net effect: a 400 with a precise, actionable, machine-readable reason from the provider is flattened into a generic string. Anyone hitting this has no path forward without reading backend logs.
The same flattening applies on the OAuth callback leg — _build_oauth_redirect (views.py:2432-2439) uses the error value only to choose between oauth_error=true and oauth_complete=true for the web flow, discarding the reason, so mcpStoreLogic.ts:859 can only show OAuth authorization failed.
Proposed fix
1. Use a charset-safe DCR client name. Change client_name to a value restricted to alphanumerics and spaces, e.g. PostHog MCP Store. This is a strict improvement in compatibility with no downside — the name is display-only metadata shown on the provider's consent screen.
2. Surface the provider's rejection reason. Give DCRRegistrationFailedError (and DCRNotSupportedError) a message populated from the provider response, and return it in the detail so the toast tells the user what the authorization server actually objected to. Keep it bounded and treat it as untrusted text. At minimum include the HTTP status and the provider's error code; the errors detail map is what makes it actionable.
3. Consider preserving the reason across the OAuth callback redirect so a failed authorize leg is diagnosable from the UI too, rather than only for the posthog-code install source.
Acceptance criteria
- A custom MCP server pointing at
https://mcp.calendly.comcompletes DCR and reaches the provider's authorize screen. - Any DCR rejection surfaces the provider's status and error code to the user, not a generic string.
- Regression test asserting the outgoing DCR
client_namematches^[A-Za-z0-9 -]+$, so a future rename cannot silently reintroduce this class of failure. - Test covering a 400 DCR response, asserting the provider's error reaches the serialized
detail.
Notes
Providers that reject our metadata look identical, from the user's side, to providers that do not support DCR at all — both dead-end at a generic toast. Fixing (2) is what makes the difference visible, and is the more valuable half of this issue: the name change fixes one provider, the error surfacing fixes every future one.
Worth auditing whether other fixed values in the DCR payload (grant_types, response_types, token_endpoint_auth_method, requested scope) have similar provider-specific strictness. Calendly, for example, ignored our requested token_endpoint_auth_method: client_secret_post and registered a public client with none — handled correctly today, but a sign that provider deviation in this payload is the norm rather than the exception.
Created with PostHog from a Slack thread
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in products/mcp_store/backend/oauth.py at the DCR payload and registration error handling, then trace _register_dcr_client_or_raise and DCRRegistrationFailedError in presentation/views.py. Check AddCustomServerForm.tsx and mcpStoreLogic.ts for how errors are rendered, and add regression coverage for the client_name and a 400 response. Done means Calendly reaches authorization and provider status and error details appear in the serialized detail.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, typescript
- Domain
- api, authentication, backend, frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100