* feat(mcp): distribution polish for agent-first onboarding: CIMD, plugin start skill, bridge hint Fourth PR of agent-first onboarding (#1814). - The OAuth AS metadata advertises client_id_metadata_document_supported next to the existing `none` token auth, the pair Claude.ai, Claude Code and Codex look for to use CIMD instead of registering a DCR client per connection. authorize/token never keyed on client_id (the redirect-URI allowlist is the trust boundary), so nothing else changes; DCR stays for ChatGPT. - The plugin's start skill no longer sends a user without an account to the website: the /mcp OAuth screen creates the account, and a NO_COMPANY_YET briefing failure routes to the onboarding skill and accounted_create_company. README updated to match. - `npx accounted-mcp` without ACCOUNTED_API_KEY prints the OAuth alternative (Claude Code, Codex, Claude.ai connector) and that the account can be created on the sign-in screen; package README too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * fix(oauth): do not advertise CIMD until redirect URIs are matched against the client document CodeRabbit on #1866: advertising client_id_metadata_document_supported makes Claude and Codex send URL client_ids and expects an exact redirect_uri match against that document; the authorize endpoint only checks the global allowlist and never fetches client metadata. The flag is withheld until an SSRF-safe, cached CIMD fetch with exact redirect matching exists. DCR stays the registration path (stateless, so free). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
35 lines
1.7 KiB
TypeScript
35 lines
1.7 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { GET } from '../route'
|
|
|
|
describe('GET /.well-known/oauth-authorization-server', () => {
|
|
beforeEach(() => {
|
|
vi.stubEnv('NEXT_PUBLIC_APP_URL', 'https://app.example.test')
|
|
})
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
it('does not advertise CIMD until redirect URIs are validated against the client document', async () => {
|
|
// Advertising client_id_metadata_document_supported makes Claude and
|
|
// Codex send URL client_ids and expects an exact redirect_uri match
|
|
// against that document; the authorize endpoint only checks the global
|
|
// allowlist today, so the flag must stay off (see the route comment).
|
|
const res = await GET(new Request('https://app.example.test/.well-known/oauth-authorization-server'))
|
|
expect(res.status).toBe(200)
|
|
const body = await res.json()
|
|
expect(body.client_id_metadata_document_supported).toBeUndefined()
|
|
expect(body.token_endpoint_auth_methods_supported).toContain('none')
|
|
// DCR (stateless register endpoint) remains the registration path.
|
|
expect(body.registration_endpoint).toBe('https://app.example.test/api/mcp-oauth/register')
|
|
expect(body.code_challenge_methods_supported).toEqual(['S256'])
|
|
expect(body.authorization_response_iss_parameter_supported).toBe(true)
|
|
})
|
|
|
|
it('does not enumerate write scopes in public discovery', async () => {
|
|
const res = await GET(new Request('https://app.example.test/.well-known/oauth-authorization-server'))
|
|
const body = await res.json()
|
|
expect(body.scopes_supported).toContain('mcp')
|
|
expect(body.scopes_supported.some((s: string) => s.endsWith(':write'))).toBe(false)
|
|
})
|
|
})
|