feat(mcp): allowlist Grok's connector callback and document the Grok path (#2158)
* feat(mcp): allowlist Grok's connector callback and document the Grok path Grok custom connectors self-register through /api/mcp-oauth/register with redirect_uri https://grok.com/connectors-oauth-exchange-code/, which the built-in allowlist rejected with invalid_redirect_uri before consent. Add the callback as an exact-path BUILT_IN_PATTERNS entry (trailing slash optional, no prefix) with provider 'grok', named "Grok (xAI)" on the consent page. Tests: accept, foreign-host and other-path rejection, provider mapping, and a register route test for the Grok DCR shape. Surface Grok next to ChatGPT: a "Using Grok?" side door on the onboarding Claude step (one side door open at a time, telemetry step grok), a Grok row under "Other clients" in the API & MCP settings tab using ?client=grok, and sv/en strings for both. Docs: mcp-server rule, ARCHITECTURE, README, registry entry (install section), DECISIONS. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EGbspj3hiNqvqTWZqdwysa Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(mcp): cite X Corp's published Grok callback, test the consent label Review pass on #2158: the allowlist comment and DECISIONS entry claimed xAI publishes no callback and the value came from a live observation; X Corp lists https://grok.com/connectors-oauth-exchange-code/ as the "Grok (web)" redirect URL at docs.x.com/x-ads-api/mcp, and grok.com serves the path itself (slash form 308s to no-slash on the same origin). Reworded both to cite that. Adds the consent-page test for "Grok (xAI)" next to the ChatGPT one and a JSDoc on the onboarding side-door toggle. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EGbspj3hiNqvqTWZqdwysa Signed-off-by: Emil <emilmattsson14@gmail.com> --------- Signed-off-by: Emil <emilmattsson14@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
61a76b1669
commit
6a85efb00a
@@ -47,6 +47,12 @@ interface NewUserChecklistProps {
|
||||
sieSweep?: { auto_linked: number; suggested: number; unmatched: number; errors: number } | null
|
||||
}
|
||||
|
||||
/** Clients that get a collapsed "Using X?" side door under the Claude step.
|
||||
* Each value keys the i18n strings step_claude_<door>_link / _steps and the
|
||||
* telemetry step name. Order is display order. */
|
||||
const SIDE_DOORS = ['chatgpt', 'grok'] as const
|
||||
type SideDoor = (typeof SIDE_DOORS)[number]
|
||||
|
||||
/**
|
||||
* Activation funnel events, mirroring the one existing product-event site
|
||||
* (lib/support/submit-feedback.ts): guarded, try/caught, no PII in
|
||||
@@ -108,9 +114,9 @@ export default function NewUserChecklist({
|
||||
// effect and re-raised the error toast forever. The next visit tries once
|
||||
// more from server truth.
|
||||
const completeRejectedRef = useRef(false)
|
||||
// The ChatGPT side door on the Claude step: collapsed by default so the
|
||||
// one-click Claude path stays the visual primary.
|
||||
const [chatGptOpen, setChatGptOpen] = useState(false)
|
||||
// The ChatGPT and Grok side doors on the Claude step: collapsed by default
|
||||
// so the one-click Claude path stays the visual primary; at most one open.
|
||||
const [sideDoor, setSideDoor] = useState<SideDoor | null>(null)
|
||||
const [serverUrlCopied, setServerUrlCopied] = useState(false)
|
||||
|
||||
const hasMigration = ENABLED_EXTENSION_IDS.has('arcim-migration')
|
||||
@@ -258,12 +264,16 @@ export default function NewUserChecklist({
|
||||
'noopener',
|
||||
)
|
||||
}
|
||||
// ChatGPT has no add-connector deep link (the user pastes the server URL
|
||||
// into Developer mode manually), so the side door copies the URL instead.
|
||||
const toggleChatGpt = () => {
|
||||
setChatGptOpen((open) => {
|
||||
if (!open) captureSetup('onboarding_setup_step_started', { step: 'chatgpt' })
|
||||
return !open
|
||||
/**
|
||||
* Open one side door (closing any other) or close it when it is already
|
||||
* open. Neither ChatGPT nor Grok has an add-connector deep link (the user
|
||||
* pastes the server URL into the client manually), so the side doors copy
|
||||
* the URL instead. Telemetry fires once per open, never on close.
|
||||
*/
|
||||
const toggleSideDoor = (door: SideDoor) => {
|
||||
setSideDoor((open) => {
|
||||
if (open !== door) captureSetup('onboarding_setup_step_started', { step: door })
|
||||
return open === door ? null : door
|
||||
})
|
||||
}
|
||||
const copyServerUrl = async () => {
|
||||
@@ -467,18 +477,23 @@ export default function NewUserChecklist({
|
||||
{t('step_claude_guide_link')}
|
||||
</a>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleChatGpt}
|
||||
aria-expanded={chatGptOpen}
|
||||
className="text-xs text-muted-foreground underline decoration-border underline-offset-4 transition-colors hover:text-foreground"
|
||||
>
|
||||
{t('step_claude_chatgpt_link')}
|
||||
</button>
|
||||
{chatGptOpen && (
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
||||
{SIDE_DOORS.map((door) => (
|
||||
<button
|
||||
key={door}
|
||||
type="button"
|
||||
onClick={() => toggleSideDoor(door)}
|
||||
aria-expanded={sideDoor === door}
|
||||
className="text-xs text-muted-foreground underline decoration-border underline-offset-4 transition-colors hover:text-foreground"
|
||||
>
|
||||
{t(`step_claude_${door}_link`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{sideDoor && (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<p className="max-w-prose text-xs leading-5 text-muted-foreground">
|
||||
{t('step_claude_chatgpt_steps', { appName })}
|
||||
{t(`step_claude_${sideDoor}_steps`, { appName })}
|
||||
</p>
|
||||
<Button size="sm" variant="outline" onClick={() => void copyServerUrl()}>
|
||||
{serverUrlCopied
|
||||
@@ -533,7 +548,7 @@ function Step({
|
||||
doneNote?: React.ReactNode
|
||||
/** Block content below the pitch while the step is open. Unlike `children`
|
||||
* (which lives inside a <p>), this may hold nested block elements, e.g.
|
||||
* the Claude step's ChatGPT side door. */
|
||||
* the Claude step's ChatGPT and Grok side doors. */
|
||||
footnote?: React.ReactNode
|
||||
last?: boolean
|
||||
children: React.ReactNode
|
||||
|
||||
@@ -422,6 +422,16 @@ export function ApiKeysPanel() {
|
||||
<CopyBlock text={mcpUrl('claude-connector')} copyAriaLabel={t('copy_aria')} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-1 text-sm">Grok</p>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
{t.rich('grok_instructions', {
|
||||
path: (chunks) => <strong>{chunks}</strong>,
|
||||
})}
|
||||
</p>
|
||||
<CopyBlock text={mcpUrl('grok')} copyAriaLabel={t('copy_aria')} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-1 text-sm">{t('claude_plugin_label')}</p>
|
||||
<p className="mb-2 text-xs text-muted-foreground">{t('claude_plugin_instructions')}</p>
|
||||
|
||||
Reference in New Issue
Block a user