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
@@ -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 (
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user