Context
We're the maintainers of the adspirer-ads-agent plugin (listed in this repo's .claude-plugin/marketplace.json, source: https://github.com/amekala/adspirer-mcp-plugin). Our plugin is a remote MCP server that runs an OAuth 2.1 flow against mcp.adspirer.com to authorize Claude clients on behalf of the user's Adspirer account.
We'd like to provide surface-aware UX for new users coming through the OAuth flow. Specifically, a first-time user installing our plugin from Claude Cowork Desktop, Claude Code Desktop, and Claude.ai Web each warrant a slightly different signup experience (different copy, different CTAs, different attribution tracking). Today we can't distinguish them.
The signal we have today
When a user clicks "Install" on our plugin, the browser receives a Claude.ai-side URL like:
https://claude.ai/api/organizations/<org-id>/mcp/start-auth/<auth-id>
?redirect_url=%2Fdesktop%2Fconnected%2Fcustomize%2Fplugins%2Fadspirer-ads-agent%2540knowledge-work-plugins%2Fconnectors%3F
&open_in_browser=1
&product_surface=claude-desktop-code
The product_surface=claude-desktop-code (or claude-desktop-cowork, etc.) is exactly the signal we need. The redirect_url path containing /desktop/connected/... is a secondary signal.
What plugin OAuth endpoints actually see
After your /api/.../mcp/start-auth/<id> endpoint processes the request and 302-redirects the browser to the plugin's /oauth/authorize endpoint, the surface signal is stripped. We see:
https://<plugin>/oauth/authorize
?response_type=code
&client_id=<plugin-registered-id>
&redirect_uri=https://claude.ai/api/mcp/auth_callback
&code_challenge=<pkce>
&code_challenge_method=S256
&session_token=<short-TTL Clerk JWT>
&scope=<space-separated>
&state=<opaque to plugin>
No product_surface. No clue about the originating surface. Plugins authoring authorize-time UX cannot distinguish Web from Desktop, or Code-Desktop from Cowork-Desktop.
Proposal
When your server constructs the redirect URL to a plugin's /oauth/authorize, append a query parameter that exposes the surface:
https://<plugin>/oauth/authorize
?response_type=code
&client_id=<plugin-registered-id>
... (existing params)
&product_surface=claude-desktop-code ← NEW
Plugin authors can opt-in to surface-aware routing. Backwards-compatible (existing plugins that ignore the param continue working unchanged).
Alternative shape (Option B in our internal analysis)
If forwarding product_surface raises concerns (e.g. privacy, or signal-mixing with OAuth-spec params), an equally-good alternative would be: register distinct client_ids per surface (e.g. claude for web, claude-desktop-code for Code Desktop, claude-desktop-cowork for Cowork Desktop). Plugins could then key their behavior off client_id directly. This is more OAuth-spec-native but a bigger change for Anthropic to roll out.
We slightly prefer the product_surface query param because it's additive and doesn't require schema migrations on the plugin side.
Use cases enabled by this
- Surface-specific signup funnels. Direct first-time-installing users to context-rich Web pages tuned to their entry path. We've seen this convert measurably better than a cold sign-in form.
- Per-surface acquisition tracking. Plugin authors can attribute new users to specific Claude surfaces (Web vs Code Desktop vs Cowork Desktop) for product analytics — same way an ad platform attributes by
utm_source.
- Per-surface UX tuning. A Desktop installer might benefit from a "you're already connected, just authorize" flow; a Web installer benefits from a "here's how this connects with Claude.ai" flow. Surface signal lets plugins make this distinction.
- Better debugging. When a user reports "install failed", surface info in the OAuth flow makes triage faster.
Privacy considerations
We don't believe this raises new privacy concerns — product_surface is non-PII metadata about the Claude UX surface, not about the user. It's directly analogous to the existing User-Agent header field for distinguishing client types.
Internal context (for cross-reference)
We've documented our internal investigation in Adspirer/adstudio#347. The current workaround is a heuristic based on session-token validity + a localStorage flag that survives session expiry. It works ~95% of the time but loses the Web-vs-Desktop distinction and the Code-vs-Cowork-Desktop distinction. Native product_surface forwarding fixes both gaps cleanly.
Related PRs in this repo
Our plugin entry update (#182) was approved by @tobinsouth on Apr 26 and dismissed after a May 10 rebase to clear merge conflict — currently REVIEW_REQUIRED. Independent of this feature request, but flagging in case the same reviewers can take a look.
Effort
Likely a 1–5 line change in the start-auth redirect handler. The signal already exists internally (we can see it in URL 1) — it just needs to be passed through to URL 2.
Happy to discuss further, contribute a PR, or test in a preview environment. Thanks for considering it.
Context
We're the maintainers of the
adspirer-ads-agentplugin (listed in this repo's.claude-plugin/marketplace.json, source: https://github.com/amekala/adspirer-mcp-plugin). Our plugin is a remote MCP server that runs an OAuth 2.1 flow againstmcp.adspirer.comto authorize Claude clients on behalf of the user's Adspirer account.We'd like to provide surface-aware UX for new users coming through the OAuth flow. Specifically, a first-time user installing our plugin from Claude Cowork Desktop, Claude Code Desktop, and Claude.ai Web each warrant a slightly different signup experience (different copy, different CTAs, different attribution tracking). Today we can't distinguish them.
The signal we have today
When a user clicks "Install" on our plugin, the browser receives a Claude.ai-side URL like:
The
product_surface=claude-desktop-code(orclaude-desktop-cowork, etc.) is exactly the signal we need. Theredirect_urlpath containing/desktop/connected/...is a secondary signal.What plugin OAuth endpoints actually see
After your
/api/.../mcp/start-auth/<id>endpoint processes the request and 302-redirects the browser to the plugin's/oauth/authorizeendpoint, the surface signal is stripped. We see:No
product_surface. No clue about the originating surface. Plugins authoring authorize-time UX cannot distinguish Web from Desktop, or Code-Desktop from Cowork-Desktop.Proposal
When your server constructs the redirect URL to a plugin's
/oauth/authorize, append a query parameter that exposes the surface:Plugin authors can opt-in to surface-aware routing. Backwards-compatible (existing plugins that ignore the param continue working unchanged).
Alternative shape (Option B in our internal analysis)
If forwarding
product_surfaceraises concerns (e.g. privacy, or signal-mixing with OAuth-spec params), an equally-good alternative would be: register distinctclient_ids per surface (e.g.claudefor web,claude-desktop-codefor Code Desktop,claude-desktop-coworkfor Cowork Desktop). Plugins could then key their behavior offclient_iddirectly. This is more OAuth-spec-native but a bigger change for Anthropic to roll out.We slightly prefer the
product_surfacequery param because it's additive and doesn't require schema migrations on the plugin side.Use cases enabled by this
utm_source.Privacy considerations
We don't believe this raises new privacy concerns —
product_surfaceis non-PII metadata about the Claude UX surface, not about the user. It's directly analogous to the existingUser-Agentheader field for distinguishing client types.Internal context (for cross-reference)
We've documented our internal investigation in Adspirer/adstudio#347. The current workaround is a heuristic based on session-token validity + a localStorage flag that survives session expiry. It works ~95% of the time but loses the Web-vs-Desktop distinction and the Code-vs-Cowork-Desktop distinction. Native
product_surfaceforwarding fixes both gaps cleanly.Related PRs in this repo
Our plugin entry update (#182) was approved by @tobinsouth on Apr 26 and dismissed after a May 10 rebase to clear merge conflict — currently
REVIEW_REQUIRED. Independent of this feature request, but flagging in case the same reviewers can take a look.Effort
Likely a 1–5 line change in the
start-authredirect handler. The signal already exists internally (we can see it in URL 1) — it just needs to be passed through to URL 2.Happy to discuss further, contribute a PR, or test in a preview environment. Thanks for considering it.