feat(mcp): distribution polish for agent-first onboarding: CIMD, plugin start skill, bridge hint (#1814 PR 4) (#1866)
* 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>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
31e0cd6e05
commit
e92365b86e
@@ -1217,3 +1217,5 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
|
||||
[2026-08-24] The MCP consent page pre-ticks companies:write for an account that has no company yet: that account is connecting in order to create a company, and a default that dead-ends on insufficient scope right after signup would be the worse default. Still an untickable checkbox, still bounded by the client's scope ceiling.
|
||||
[2026-08-25] gnubok_create_company / POST /api/v1/companies require f_skatt explicitly and org_number whenever vat_registered (review findings on #1864): a defaulted F-skatt approval or a VAT-registered company with no momsregistreringsnummer would flow straight into invoices (ML 17 kap 24 §, SE-R-005). Explicit beats convenient on a legal fact.
|
||||
[2026-08-25] An enskild firma's first fiscal year must end on 31 December in the programmatic setup paths, mirroring the wizard's own rule text: the calendar-year mandate (BFL 3 kap. 1 §) is not lifted by the first-year extension.
|
||||
[2026-08-24] The OAuth AS metadata advertises client_id_metadata_document_supported (CIMD, #1814 PR 4) without fetching or validating the client's metadata document: authorize/token never keyed anything on client_id, the redirect_uri allowlist is the trust boundary, and CIMD only changes what Claude/Codex send as client_id (an HTTPS URL instead of a DCR-minted UUID). Fetching the document would add a network dependency to every consent for no gain in this design. DCR stays for ChatGPT.
|
||||
[2026-08-25] CIMD is NOT advertised after all (reverses the 2026-08-24 entry; CodeRabbit on #1866): the spec expects an AS that advertises client_id_metadata_document_supported to fetch the document and match redirect_uri exactly against it, and our authorize endpoint only checks the global allowlist. Advertising would claim a check we skip. Add the flag together with an SSRF-safe cached CIMD fetch + exact redirect matching (localhost port-agnostic for Claude Code/Codex); DCR is free for us (stateless register), so nothing is lost meanwhile.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -23,6 +23,16 @@ export async function GET(request: Request) {
|
||||
grant_types_supported: ['authorization_code', 'refresh_token'],
|
||||
code_challenge_methods_supported: ['S256'],
|
||||
token_endpoint_auth_methods_supported: ['none', 'client_secret_post'],
|
||||
// Client ID Metadata Documents (MCP auth spec 2025-11-25) are deliberately
|
||||
// NOT advertised yet. Advertising the flag makes Claude.ai, Claude Code
|
||||
// and Codex send an HTTPS URL as client_id, and the spec then expects the
|
||||
// authorization server to fetch that document and match redirect_uri
|
||||
// exactly against its redirect_uris. Our authorize endpoint validates
|
||||
// redirect_uri against the global allowlist only (lib/auth/oauth-allowlist.ts)
|
||||
// and never fetches client metadata, so advertising CIMD would claim a
|
||||
// check we do not perform. The stateless register endpoint makes DCR
|
||||
// free for us, so nothing is lost by waiting: add the flag together with
|
||||
// an SSRF-safe, cached CIMD fetch and exact redirect matching.
|
||||
// RFC 9207: the authorize endpoint includes `iss` in every authorization
|
||||
// response (success and error) so clients can detect mix-up attacks.
|
||||
authorization_response_iss_parameter_supported: true,
|
||||
|
||||
@@ -12,7 +12,7 @@ The official plugin for [Accounted](https://app.accounted.se), the open-source S
|
||||
/plugin install accounted@accounted
|
||||
```
|
||||
|
||||
Then run `/mcp` and authenticate with Accounted (OAuth consent screen; read-only scopes by default, write scopes are ticked explicitly). Start with `/accounted:start`.
|
||||
Then run `/mcp` and authenticate with Accounted (OAuth consent screen; read-only scopes by default, write scopes are ticked explicitly). No account yet? Create it on that same screen, with BankID or e-mail. Start with `/accounted:start`: for a brand-new account it walks you through setting up the company (company form, organisationsnummer, VAT, fiscal year) right here in the conversation, then hands you the bank and Skatteverket connect links.
|
||||
|
||||
## Skills
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ Verify the connection, learn who this company is, and surface what needs attenti
|
||||
## Flow
|
||||
|
||||
1. Call `accounted_get_agent_briefing`. This is the single source for company facts: entity type (aktiebolag or enskild firma), accounting method (faktureringsmetoden or kontantmetoden), VAT period, employees, and ledger context. Never assume these; the flows below behave differently depending on them.
|
||||
- If the call fails with an auth error, the MCP server is not connected yet: tell the user to run `/mcp` and authenticate with Accounted (OAuth consent screen; read-only scopes by default, write scopes are ticked explicitly). Self-hosted users: see the plugin README.
|
||||
- If the user has no Accounted account at all, say so plainly and point them at https://app.accounted.se to create one. The plugin drives an existing ledger; it cannot bookkeep without one. Do not attempt any other flow until a company is connected.
|
||||
- If the call fails with an auth error, the MCP server is not connected yet: tell the user to run `/mcp` and authenticate with Accounted (OAuth consent screen; read-only scopes by default, write scopes are ticked explicitly). A user who has no Accounted account creates it on that same screen (BankID or e-mail, about a minute); nobody needs to visit the website first. Self-hosted users: see the plugin README.
|
||||
- If the call fails with `NO_COMPANY_YET`, the account exists but has no company: this is a brand-new user. Load `accounted_load_skill("onboarding")` and follow it. It gathers the facts (company form, organisationsnummer, VAT and moms period, accounting method, fiscal year), previews and creates the company with `accounted_create_company`, then hands out the bank and Skatteverket connect links. Do not attempt any other flow until the company exists.
|
||||
2. Read `Accounted://attention` and `Accounted://period/active`.
|
||||
3. Present a short orientation in the user's language: company name and form, active fiscal period and its lock status, and the top 3 items needing attention.
|
||||
4. Point at the flows, matched to what attention showed:
|
||||
|
||||
@@ -41,15 +41,31 @@ compatibility. Only the MCP integration is being renamed in this release.
|
||||
The API key scopes determine which tools are visible and callable. Write tools
|
||||
stage pending operations for explicit approval before anything is booked.
|
||||
|
||||
## OAuth connector
|
||||
## OAuth connector (no API key, no account needed up front)
|
||||
|
||||
Clients with OAuth custom-connector support can connect directly without this
|
||||
bridge:
|
||||
Clients with OAuth support connect directly without this bridge and without an
|
||||
existing API key. The sign-in screen lets a new user create the Accounted
|
||||
account (BankID or e-mail), and company setup then continues in the
|
||||
conversation through the `onboarding` skill and `accounted_create_company`:
|
||||
|
||||
```text
|
||||
https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted
|
||||
```
|
||||
|
||||
```bash
|
||||
# Claude Code
|
||||
claude mcp add --transport http accounted \
|
||||
"https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted"
|
||||
|
||||
# OpenAI Codex
|
||||
codex mcp add accounted --url \
|
||||
"https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted"
|
||||
```
|
||||
|
||||
Claude.ai and Claude Desktop: Settings > Connectors > Add custom connector,
|
||||
paste the URL. The connector works before you connect (documentation tools);
|
||||
the first company-scoped call opens the Connect prompt.
|
||||
|
||||
## Compatibility
|
||||
|
||||
The legacy `gnubok-mcp` package, environment variables, endpoint behavior, and
|
||||
|
||||
@@ -54,6 +54,14 @@ if (!API_KEY) {
|
||||
'Error: ACCOUNTED_API_KEY is required.\n' +
|
||||
'Get your API key at: https://app.accounted.se/settings?tab=api\n' +
|
||||
'\n' +
|
||||
'No API key (or no account yet)? Connect over OAuth instead; the sign-in\n' +
|
||||
'screen lets you create the account, and setup continues in the chat:\n' +
|
||||
' claude mcp add --transport http accounted \\\n' +
|
||||
' "https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted"\n' +
|
||||
' codex mcp add accounted --url \\\n' +
|
||||
' "https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted"\n' +
|
||||
' Claude.ai / Desktop: Settings > Connectors > Add custom connector with that URL.\n' +
|
||||
'\n' +
|
||||
'Add it to your Claude Desktop config:\n' +
|
||||
'{\n' +
|
||||
' "mcpServers": {\n' +
|
||||
|
||||
Reference in New Issue
Block a user