fix(mcp): eager-auth flag on the Grok connector links so Grok starts OAuth (#2167)

* fix(mcp): eager-auth flag on the Grok connector links so Grok starts OAuth

Live test after #2158: pasting the Grok URL into grok.com's custom
connector dialog listed all 150+ tools and never opened the sign-in. Grok
probes the URL without credentials, like claude.ai, and reads the lazy
200 on initialize as an authless server; only the 401 challenge starts
OAuth (#2159 fixed the same thing for the claude.ai link).

- lib/onboarding/checklist.ts: mcpServerUrl() builds the server URL with
  an optional eagerAuth flag; sideDoorServerUrl() gives the Grok side door
  auth=required and keeps ChatGPT lazy; claudeConnectorLink() reuses it.
  SIDE_DOORS / SideDoor move here from the component. Tests for all three.
- NewUserChecklist copies the door-specific URL (now with a client marker).
- ApiKeysPanel's Grok row copies the flagged URL, mirroring the Claude one.
- auth-mode.ts comment records the second consumer; registry entry's Grok
  step carries the flag; DECISIONS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhTJcwgzmN3TsLR8tVHwdi
Signed-off-by: Emil <emilmattsson14@gmail.com>

* docs(mcp): registry Claude.ai step carries auth=required too

Review pass on #2167: the registry entry flagged the Grok install URL
but left the Claude.ai step on the bare URL, which pre-fills "None" in
claude.ai's dialog (#2159). Same file, same flag, now consistent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhTJcwgzmN3TsLR8tVHwdi
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:
Mattsson
2026-09-02 16:55:23 +02:00
committed by GitHub
parent f1230282a9
commit a80ce54b78
7 changed files with 96 additions and 23 deletions
+1
View File
@@ -1495,3 +1495,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-09-02] Removed the skattekonto drift email (skattekonto.drift_detected event, handler, /api/extensions/skatteverket/skattekonto/drift route, cron hook) instead of fixing it: it alerted on raw saldo-vs-1630 gaps that unbooked rows explain by construction (2026-09-02: Arcim 35 842 kr, 100% explained, while the Hem notice and reconciliation page said nothing was wrong), repeated every 24 h, and was the only surface of a May-2026 feature whose promised dashboard tile was never built. Since 2026-08-25 the reconciliation page and the Hem notice (detectSkvUnexplained, gated on unexplained_difference) are the surface. Considered gating the mail on unexplained_difference + once per episode (built, then dropped): after that gate it only fires on integrity findings the engine itself calls 'never a user task'. skattekonto_drift_tolerance stays (Hem notice reads it); stale skattekonto_drift_last_alert_at rows in extension_data are inert.
[2026-09-02] parties phase 0, golden set stays out of git: the labelling sample is prod voucher text with person names (salary, expense claims) and the repo is public, so the draw SQL is versioned but the rows and labels live in gitignored dev_docs/parties/golden/.
[2026-09-02] MCP eager-auth flag (`auth=required`) on the claude.ai connector links instead of reverting lazy auth: claude.ai's two-step Add-custom-connector dialog probes the URL without credentials and pre-fills Authentication "None" when the lazy handshake answers 200, which blocks the sign-in later; per Anthropic's docs a 401 is the only answer it reads as OAuth. The flag lives in the URL, so the links we control (Settings, onboarding checklist, both docs pages, website) get OAuth detected while the bare URL keeps lazy auth for Claude Code, the plugin, Cursor and ChatGPT, and existing connector records stay untouched. Rejected: keying eager auth off `client=claude-connector` (documented as telemetry-only) and sniffing the probe's user agent (fragile, undocumented).
[2026-09-02] Grok links carry auth=required like the claude.ai link (#2159), decided from a live test: on the lazy URL Grok's connector dialog listed all 150+ tools and never opened the sign-in, so it reads the 200 probe as an authless server exactly as claude.ai does. The flag lives in one helper (mcpServerUrl / sideDoorServerUrl in lib/onboarding/checklist.ts) so the settings row, the onboarding side door and the deep link cannot drift; ChatGPT stays lazy because its developer mode honours the 401 on the first protected call.
+6 -9
View File
@@ -17,6 +17,9 @@ import {
checklistNumbers,
claudeConnectorLink,
completionPatchBody,
SIDE_DOORS,
sideDoorServerUrl,
type SideDoor,
type VatDeadlineLine,
} from '@/lib/onboarding/checklist'
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
@@ -47,12 +50,6 @@ 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
@@ -276,8 +273,8 @@ export default function NewUserChecklist({
return open === door ? null : door
})
}
const copyServerUrl = async () => {
const serverUrl = `${window.location.origin}/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted`
const copyServerUrl = async (door: SideDoor) => {
const serverUrl = sideDoorServerUrl({ origin: window.location.origin, door })
try {
await navigator.clipboard.writeText(serverUrl)
setServerUrlCopied(true)
@@ -495,7 +492,7 @@ export default function NewUserChecklist({
<p className="max-w-prose text-xs leading-5 text-muted-foreground">
{t(`step_claude_${sideDoor}_steps`, { appName })}
</p>
<Button size="sm" variant="outline" onClick={() => void copyServerUrl()}>
<Button size="sm" variant="outline" onClick={() => void copyServerUrl(sideDoor)}>
{serverUrlCopied
? t('step_claude_chatgpt_copied')
: t('step_claude_chatgpt_copy')}
+4 -1
View File
@@ -320,6 +320,9 @@ export function ApiKeysPanel() {
// (extensions/general/mcp-server/auth-mode.ts). Claude Code, Cursor and the
// stdio bridge keep the lazy URL.
const claudeConnectorUrl = `${mcpUrl('claude-connector')}&auth=required`
// Grok's custom-connector dialog does the same probe: on the lazy URL it
// lists every tool and never opens the sign-in (observed 2026-09-02).
const grokConnectorUrl = `${mcpUrl('grok')}&auth=required`
// claude.ai install link: opens Add-custom-connector with name and URL
// prefilled. It only prefills the dialog, so the user still reviews and
@@ -436,7 +439,7 @@ export function ApiKeysPanel() {
path: (chunks) => <strong>{chunks}</strong>,
})}
</p>
<CopyBlock text={mcpUrl('grok')} copyAriaLabel={t('copy_aria')} />
<CopyBlock text={grokConnectorUrl} copyAriaLabel={t('copy_aria')} />
</div>
<div>
+6 -3
View File
@@ -7,8 +7,8 @@
* turns into its Connect prompt.
*
* `auth=required` on the endpoint URL makes that URL eager instead: EVERY
* tokenless request answers the 401 challenge, `initialize` included. It has
* one consumer. claude.ai's two-step "Add custom connector" dialog probes the
* tokenless request answers the 401 challenge, `initialize` included. Two
* consumers. claude.ai's two-step "Add custom connector" dialog probes the
* URL without credentials and pre-fills the Authentication choice from the
* answer (Anthropic: "Claude checks the URL and pre-fills the authentication
* settings it detects"). A 200 on that probe is read as "None", an authless
@@ -16,7 +16,10 @@
* when the challenge arrives later. A 401 is the only answer the dialog reads
* as OAuth (Anthropic: "Claude does not honor a WWW-Authenticate header on a
* 200 response"), so the links we control (Settings -> API & MCP, the
* onboarding checklist, both docs pages, the website) carry the flag.
* onboarding checklist, both docs pages, the website) carry the flag. Grok's
* custom-connector dialog behaves the same way: on the lazy URL it lists
* every tool and never starts OAuth (observed 2026-09-02), so the Grok links
* carry the flag too.
*
* The bare URL keeps lazy authentication for Claude Code, the plugin, Cursor,
* ChatGPT developer mode and hand-typed adds, and connector records created
@@ -2,6 +2,9 @@ import { describe, expect, it } from 'vitest'
import {
checklistNumbers,
claudeConnectorLink,
mcpServerUrl,
sideDoorServerUrl,
SIDE_DOORS,
claudeStepDone,
completionPatchBody,
vatDeadlineLine,
@@ -105,6 +108,39 @@ describe('claudeStepDone', () => {
})
})
describe('mcpServerUrl', () => {
it('builds the namespaced URL with the client marker and no auth flag by default', () => {
expect(mcpServerUrl({ origin: 'https://app.testbrand.example', client: 'cursor' })).toBe(
'https://app.testbrand.example/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted&client=cursor',
)
})
it('appends auth=required when eagerAuth is set', () => {
const url = new URL(mcpServerUrl({ origin: 'http://localhost:3000', client: 'grok', eagerAuth: true }))
expect(url.searchParams.get('tool_namespace')).toBe('accounted')
expect(url.searchParams.get('client')).toBe('grok')
expect(url.searchParams.get('auth')).toBe('required')
})
})
describe('sideDoorServerUrl', () => {
it('lists chatgpt then grok', () => {
expect(SIDE_DOORS).toEqual(['chatgpt', 'grok'])
})
it('gives Grok the eager-auth flag: its dialog reads a 200 probe as "no auth" and never starts OAuth', () => {
const url = new URL(sideDoorServerUrl({ origin: 'https://app.testbrand.example', door: 'grok' }))
expect(url.searchParams.get('client')).toBe('grok')
expect(url.searchParams.get('auth')).toBe('required')
})
it('keeps ChatGPT on the lazy URL', () => {
const url = new URL(sideDoorServerUrl({ origin: 'https://app.testbrand.example', door: 'chatgpt' }))
expect(url.searchParams.get('client')).toBe('chatgpt')
expect(url.searchParams.get('auth')).toBeNull()
})
})
describe('claudeConnectorLink', () => {
it('builds the claude.ai deep link with namespace, client marker and eager-auth flag, from the page origin', () => {
const link = claudeConnectorLink({ origin: 'https://app.testbrand.example', appName: 'Testbrand' })
+41 -8
View File
@@ -76,18 +76,51 @@ export function claudeStepDone(input: { oauthKeyCount: number | null | undefined
return (input.oauthKeyCount ?? 0) > 0
}
/**
* The MCP server URL we hand to a client. `tool_namespace` is load-bearing
* (without it the server hands out legacy `gnubok_` tool names), `client`
* is a telemetry-only distribution marker, and the origin comes from the
* page so self-hosted and white-label domains link to themselves.
*
* `eagerAuth` appends `auth=required` (extensions/general/mcp-server/
* auth-mode.ts). Needed for clients whose Add-connector dialog probes the
* URL without credentials and reads the lazy 200 as "no authentication":
* claude.ai pre-fills "None" and Grok lists every tool without ever opening
* the sign-in. The 401 challenge is the only answer those dialogs read as
* OAuth. ChatGPT developer mode, Claude Code, Cursor and the stdio bridge
* keep the lazy URL.
*/
export function mcpServerUrl(input: { origin: string; client: string; eagerAuth?: boolean }): string {
const eager = input.eagerAuth ? '&auth=required' : ''
return `${input.origin}/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted&client=${input.client}${eager}`
}
/**
* Clients that get a collapsed "Using X?" side door under the checklist's
* Claude step. Each value keys the i18n strings step_claude_<door>_link /
* _steps and the telemetry step name. Order is display order.
*/
export const SIDE_DOORS = ['chatgpt', 'grok'] as const
export type SideDoor = (typeof SIDE_DOORS)[number]
/**
* The URL a side door copies. Grok's connector dialog behaves like
* claude.ai's (a 200 probe means "no auth", so the OAuth flow never starts)
* and needs the eager flag; ChatGPT's developer mode honours the lazy 401
* on the first protected call and keeps the plain URL.
*/
export function sideDoorServerUrl(input: { origin: string; door: SideDoor }): string {
return mcpServerUrl({ origin: input.origin, client: input.door, eagerAuth: input.door === 'grok' })
}
/**
* The claude.ai Add-custom-connector deep link the checklist's Claude step
* opens. Same shape as the Settings → API & MCP button: `tool_namespace` is
* load-bearing (without it the server hands out legacy `gnubok_` tool
* names), `client` is a telemetry-only distribution marker, `auth=required`
* makes claude.ai's dialog detect OAuth instead of "None" (see
* extensions/general/mcp-server/auth-mode.ts), and the origin comes from the
* page so self-hosted and white-label domains link to themselves. The link
* only prefills the dialog; the user reviews there.
* opens. Same shape as the Settings → API & MCP button (see mcpServerUrl for
* the query parameters). The link only prefills the dialog; the user reviews
* there.
*/
export function claudeConnectorLink(input: { origin: string; appName: string }): string {
const serverUrl = `${input.origin}/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted&client=claude-connector&auth=required`
const serverUrl = mcpServerUrl({ origin: input.origin, client: 'claude-connector', eagerAuth: true })
return (
'https://claude.ai/customize/connectors?modal=add-custom-connector' +
`&connectorName=${encodeURIComponent(input.appName)}` +
+2 -2
View File
@@ -65,14 +65,14 @@ verktyg den får exponera.
## Installera i Claude.ai
1. I Claude → Settings → Connectors → Add custom connector.
2. Ange URL: `https://app.gnubok.se/api/extensions/ext/mcp-server/mcp`.
2. Ange URL: `https://app.gnubok.se/api/extensions/ext/mcp-server/mcp?auth=required` (utan `auth=required` föreslår dialogen "None" som autentisering och inloggningen öppnas aldrig).
3. Claude öppnar accounted-OAuth i webbläsaren. Logga in. Godkänn anslutningen.
4. Connectorn dyker upp i listan. Slå på för de chattar där du vill ha den aktiv.
## Installera i Grok
1. På grok.com: Connectors → New Connector → Custom.
2. Ange URL: `https://app.gnubok.se/api/extensions/ext/mcp-server/mcp`.
2. Ange URL: `https://app.gnubok.se/api/extensions/ext/mcp-server/mcp?auth=required` (utan `auth=required` ser Grok servern som öppen och startar aldrig inloggningen).
3. Grok registrerar sig själv och öppnar accounted-OAuth. Logga in. Godkänn anslutningen.
4. Starta en ny chatt med connectorn påslagen.