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:
Mattsson
2026-09-02 14:42:39 +02:00
committed by GitHub
co-authored by Claude Fable 5.1
parent 61a76b1669
commit 6a85efb00a
14 changed files with 131 additions and 32 deletions
@@ -40,6 +40,7 @@ import { GET, POST } from '../route'
const CLAUDE: RedirectUriResolution = { allowed: true, kind: 'built_in', provider: 'claude' }
const CHATGPT: RedirectUriResolution = { allowed: true, kind: 'built_in', provider: 'chatgpt' }
const GROK: RedirectUriResolution = { allowed: true, kind: 'built_in', provider: 'grok' }
const REGISTERED: RedirectUriResolution = {
allowed: true,
kind: 'registered',
@@ -364,6 +365,25 @@ describe('client identity on the consent page', () => {
expect(html).toContain('chatgpt.com')
})
it('names Grok as a verified client for the grok.com callback', async () => {
mocks.resolveRedirectUri.mockResolvedValue(GROK)
const html = await (
await GET(
new Request(
buildAuthorizeUrl({
...params,
redirect_uri: 'https://grok.com/connectors-oauth-exchange-code/',
}),
),
)
).text()
expect(html).toContain('Grok (xAI)')
expect(html).toContain('Verifierad')
expect(html).toContain('grok.com')
expect(html).not.toContain('En extern applikation')
})
it('shows client_name and redirect host for a DB-registered client, never marked verified', async () => {
mocks.resolveRedirectUri.mockResolvedValue(REGISTERED)
const html = await (
+2
View File
@@ -1092,6 +1092,8 @@ function describeClient(
return { name: 'Claude (Anthropic)', tag: 'Verifierad', verified: true }
case 'chatgpt':
return { name: 'ChatGPT (OpenAI)', tag: 'Verifierad', verified: true }
case 'grok':
return { name: 'Grok (xAI)', tag: 'Verifierad', verified: true }
case 'local':
return { name: 'Lokal utveckling (localhost)', tag: 'Din egen dator', verified: false }
}
@@ -51,6 +51,25 @@ describe('POST /api/mcp-oauth/register', () => {
expect(response.status).toBe(201)
})
it('accepts registration with the grok.com connector callback', async () => {
const response = await POST(createRequest({
client_name: 'Grok',
redirect_uris: ['https://grok.com/connectors-oauth-exchange-code/'],
token_endpoint_auth_method: 'none',
}))
expect(response.status).toBe(201)
const body = await response.json()
expect(body.redirect_uris).toEqual(['https://grok.com/connectors-oauth-exchange-code/'])
expect(body.token_endpoint_auth_method).toBe('none')
})
it('rejects other grok.com paths', async () => {
const response = await POST(createRequest({
redirect_uris: ['https://grok.com/oauth/callback'],
}))
expect(response.status).toBe(400)
})
it('rejects registration with disallowed redirect_uris', async () => {
const response = await POST(createRequest({
redirect_uris: ['https://evil.com/callback'],