* 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>
108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { POST } from '../route'
|
|
|
|
function createRequest(body: unknown) {
|
|
return new Request('http://localhost/api/mcp-oauth/register', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
}
|
|
|
|
describe('POST /api/mcp-oauth/register', () => {
|
|
it('returns 400 for invalid JSON', async () => {
|
|
const request = new Request('http://localhost/api/mcp-oauth/register', {
|
|
method: 'POST',
|
|
body: 'not json',
|
|
})
|
|
const response = await POST(request)
|
|
expect(response.status).toBe(400)
|
|
})
|
|
|
|
it('accepts registration with valid claude.ai redirect_uris', async () => {
|
|
const response = await POST(createRequest({
|
|
client_name: 'Test Client',
|
|
redirect_uris: ['https://claude.ai/api/oauth/callback'],
|
|
}))
|
|
expect(response.status).toBe(201)
|
|
const body = await response.json()
|
|
expect(body.client_id).toBeDefined()
|
|
expect(body.redirect_uris).toEqual(['https://claude.ai/api/oauth/callback'])
|
|
})
|
|
|
|
it('accepts registration with localhost redirect_uris', async () => {
|
|
const response = await POST(createRequest({
|
|
redirect_uris: ['http://localhost:3000/callback'],
|
|
}))
|
|
expect(response.status).toBe(201)
|
|
})
|
|
|
|
it('accepts registration with 127.0.0.1 redirect_uris', async () => {
|
|
const response = await POST(createRequest({
|
|
redirect_uris: ['http://127.0.0.1:8080/callback'],
|
|
}))
|
|
expect(response.status).toBe(201)
|
|
})
|
|
|
|
it('accepts registration with claude.com redirect_uris', async () => {
|
|
const response = await POST(createRequest({
|
|
redirect_uris: ['https://claude.com/api/oauth/callback'],
|
|
}))
|
|
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'],
|
|
}))
|
|
expect(response.status).toBe(400)
|
|
const body = await response.json()
|
|
expect(body.error).toBe('invalid_redirect_uri')
|
|
})
|
|
|
|
it('rejects if any redirect_uri in array is invalid', async () => {
|
|
const response = await POST(createRequest({
|
|
redirect_uris: [
|
|
'https://claude.ai/api/callback',
|
|
'https://evil.com/steal',
|
|
],
|
|
}))
|
|
expect(response.status).toBe(400)
|
|
})
|
|
|
|
it('accepts registration with no redirect_uris', async () => {
|
|
const response = await POST(createRequest({
|
|
client_name: 'No URIs',
|
|
}))
|
|
expect(response.status).toBe(201)
|
|
const body = await response.json()
|
|
expect(body.redirect_uris).toEqual([])
|
|
})
|
|
|
|
it('defaults client_name to MCP Client', async () => {
|
|
const response = await POST(createRequest({}))
|
|
expect(response.status).toBe(201)
|
|
const body = await response.json()
|
|
expect(body.client_name).toBe('MCP Client')
|
|
})
|
|
})
|