PostHog / PostHog/posthog

MCP store: hardcoded DCR client_name breaks OAuth against strict providers (e.g. Calendly), and the provider's error is swallowed

Open
#89,998 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug self-driving
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.

  1. register_dcr_client logs DCR registration request rejected with status and body (oauth.py:339-345), then calls resp.raise_for_status().
  2. _register_dcr_client_or_raise (presentation/views.py:968-981) catches the resulting HTTPError under a bare except Exception, logs DCR registration failed, and raises DCRRegistrationFailedError.
  3. DCRRegistrationFailedError carries no message (views.py:105-110), so the handler in _authorize_for_custom / install_template responds with a fixed {"detail": "OAuth registration failed."}.
  4. The frontend renders e.detail verbatim (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.com completes 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_name matches ^[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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.