docs(api,mcp): tool counts, changelog backfill, version-header honesty, lazy auth, endpoint map (#1929)

Brings the developer-facing API and MCP docs back in line with origin/main
(audit 2026-08-26). Docs only; no runtime behaviour changes.

- Tool counts: the server registers 153 tools; docs said 90+/100+/120.
  All now say "150+" (connect-claude, gnubok-mcp README, plugin README,
  mcp-server rules, CLAUDE.md, registry entry with refreshed updatedAt).
  Not derived from the tools array: lib/ must not import @/extensions/.
- REST changelog: backfilled the additive 2026-08 changes (#1909 report
  date ranges + PDFs, #1864 POST /companies, #1773 vat-declarations,
  #1405 PATCH settings, #1724/#1788 customer personal_number, #1809
  cash_account_id filter). API version date unchanged.
- Version headers: Gnubok-Deprecation is planned, not emitted; the
  Gnubok-Version request header is not read today (version.ts comment,
  versioning page, conventions overlay, regenerated skills/accounted-api).
- connect-claude Path A documents lazy auth (connector works before an
  account exists; sign-in on the first company-scoped call).
- MCP server README: real Anthropic SDK call sites, real resource URIs,
  pending-operations widget, public-tools/tasks/origin-guard/pii-guard.
  Rules file gains Lazy auth + feedback/tasks paragraphs.
- api-routes endpoint map regenerated from the filesystem (560 routes,
  55 families incl. v1, agent, reconciliation account-keyed, dimensions,
  peppol, rot-rut, webshop-orders, mileage, billing, skatteverket,
  receipt-hunt).
- gnubok-mcp/accounted-mcp: /settings?tab=api is the pre-redesign URL;
  now /settings/api (README + help hints, no version bump).

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:
Jakob Wennberg
2026-08-26 13:35:54 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent b8605aabfc
commit 188816652d
16 changed files with 89 additions and 48 deletions
+25 -14
View File
@@ -44,17 +44,28 @@ export const POST = withRouteContext<{ params: Promise<{ id: string }> }>(
## Endpoint map (`app/api/`)
- `/api/bookkeeping/*`: accounts, fiscal periods, journal entries (CRUD/reverse/correct), mapping rules, voucher gaps
- `/api/invoices/*`, `/api/supplier-invoices/*`: CRUD + state transitions
- `/api/transactions/*`: categorize, describe, book, match-{invoice,supplier-invoice}, batch, AI suggestions
- `/api/customers/*`, `/api/suppliers/*`: CRUD
- `/api/documents/*`: CRUD, versions, link, match-sweep, verify cron
- `/api/reports/*`: report endpoints (GL, TB, BS, IS, AR/supplier ledger, VAT, SIE, INK2, NE-bilaga, KPI, audit, continuity, monthly, full-archive, salary, vacation, avgifter)
- `/api/salary/*`: employees, payroll-config, tax-tables, KU, runs
- `/api/import/*`: bank-file, SIE (parse/execute/mappings)
- `/api/reconciliation/bank/*`, `/api/settings/*`, `/api/company/*`, `/api/team/*`
- `/api/deadlines/*`, `/api/tax-deadlines/*`: CRUD + crons
- `/api/pending-operations/*`, `/api/events/*`, `/api/audit-trail/*`
- `/api/calendar/feed/[token]`, `/api/mcp-oauth/*`, `/api/support/contact`, `/api/account/delete`
- `/api/log`, `/api/health`, `/api/vat/validate`, `/api/currency/rate`, `/api/sandbox/*`
- `/api/extensions/ext/[...path]`: dynamic extension routes (catch-all → `/api/extensions/ext/{extensionId}/{routePath}`, path params as `_paramName` query)
560 `route.ts` files under 55 top-level families (2026-08-26). Counts in parentheses; regenerate with `find app/api -name route.ts | awk -F/ '{print $3}' | sort | uniq -c`.
- `/api/v1/*` (110): the public API-key REST surface (`withApiV1`, `lib/api/v1/`). Companies (list + create), customers, invoices, suppliers, supplier-invoices, transactions, journal-entries, fiscal-periods, accounts, articles, documents, dimensions, employees, salary-runs, reports (16, incl. balance-sheet/income-statement PDFs), reconciliation (11, account-keyed), imports, operations, compliance, skatteverket/vat-declarations, settings, inbox-items, voucher-gap-explanations, webhooks, webhook-deliveries, openapi.json, health
- `/api/bookkeeping/*` (64): accounts, account-balances, account-totals, fiscal-periods, journal-entries (CRUD/reverse/correct), journal-entry-lines, mapping-rules, accruals, voucher-gaps, voucher-sequences, no-doc-required, fix-cash-mismatch
- `/api/reports/*` (58): GL, TB, BS/IS (+ balansrapport/resultatrapport), AR/supplier ledger, VAT, periodisk sammanställning, SIE, INK2, NE-bilaga, KPI, audit-trail, behandlingshistorik, bokslutsbilagor, continuity, monthly, dimension-pnl, kassaflödesanalys, statement-reconciliation, full-archive, salary-journal, vacation-liability, avgifter-basis
- `/api/salary/*` (36): employees, payroll-config, tax-tables, KU, runs, vacation-balances, vacation-year-close
- `/api/invoices/*` (28), `/api/supplier-invoices/*` (16): CRUD + state transitions, bulk-book, recurring, reminders, self-billed, preview-pdf, next-number; supplier side adds exists + payment-batches (betalfil)
- `/api/import/*` (26): bank-file, SIE (parse/execute/mappings), skattekonto-file, opening-balance, articles, customers, suppliers, documents
- `/api/extensions/*` (21): `ext/[...path]` dynamic extension routes (catch-all → `/api/extensions/ext/{extensionId}/{routePath}`, path params as `_paramName` query), `[sector]` listing, plus first-party callbacks (enable-banking, cloud-backup, invoice-inbox, whatsapp-inbox, shopify, woocommerce, stripe, skatteverket, push-notifications)
- `/api/transactions/*` (20): list/detail, categorize/uncategorize, book, ignore, match-{invoice,supplier-invoice} (+ preview), match-batch, batch-match-invoices, bulk-book, create-from-document, suggest-categories, attach-document, link-journal-entry, cash-account, duplicate-payment-check, refresh-exchange-rate
- `/api/settings/*` (19): company settings, api-keys, oauth-clients, booking-templates, counterparty-templates, logo, invoice-font, peppol, signals (eu-trade, ku, rot-rut)
- `/api/reconciliation/*` (18): `accounts` + `accounts/[accountKey]/*` (account-keyed engine, #1833: bridge, items + ignore, links, attachments, residual, signoff + reopen) and the legacy `bank/*` verbs (run, status, link/unlink, unmatched-entries, confirm-suggestions, mark-opening-balance)
- `/api/agent/*` (16): ask, invoke, composer, conversations (+ reject-pending), categorize (+ outcome), memory, knowledge, skills, profile (+ verify), feedback, onboarding/stream
- `/api/documents/*` (12): CRUD, versions, inline, link/detach, integrity, extraction-status, verify (+ cron), counts, inbox-available
- `/api/company/*` (10), `/api/team/*` (5), `/api/user/*` (4), `/api/account/*` (2): company CRUD, current, members + invites, check-org-number, migration-reset; team invite/accept/members; preferences, profile, locale, ui-state; delete, password
- `/api/dimensions/*` (9): CRUD, rules, tagging, import-existing
- `/api/pending-operations/*` (7): list/detail, bulk-commit, bulk-reject, expire
- `/api/mileage/*` (6), `/api/rot-rut/*` (6), `/api/webshop-orders/*` (6): trips, distance, book, export, salary-push; eligible, beslut, payout-requests, payout-file; orders + bulk-book + settings
- `/api/deadlines/*` (5), `/api/tax-deadlines/*` (2), `/api/tax-assessment-notices/*` (2), `/api/skatteverket/*` (3): deadline CRUD + status, tax-deadline generate + cron, assessment notices, skattekonto tax-payments
- `/api/customers/*` (3, incl. `[id]/personal-number`), `/api/suppliers/*` (2), `/api/articles/*` (2), `/api/assets/*` (3), `/api/cash-accounts`, `/api/export/*` (3: articles, customers, suppliers)
- `/api/billing/*` (3: checkout, portal, status), `/api/stripe/webhook`
- `/api/peppol/*` (2: inbound, outbound), `/api/webhooks/*` (2: dispatch cron, peppol/qvalia callback)
- `/api/receipt-hunt/*` (2: run, cron), `/api/sandbox/*` (2: seed, cleanup), `/api/events/*` (2), `/api/notices/*` (2), `/api/idempotency/cleanup/cron`
- `/api/mcp-oauth/*` (3: authorize, token, register), `/api/calendar/feed/[token]`, `/api/payslip/[token]/pdf`, `/api/storage/[...path]`
- Singletons: `/api/audit-trail`, `/api/auth/heartbeat`, `/api/currency/rate`, `/api/health`, `/api/kpi/preferences`, `/api/log`, `/api/onboarding/state`, `/api/support/contact`, `/api/vat/validate`, `/api/version`, `/api/worklist/counts`
+5 -1
View File
@@ -9,7 +9,7 @@ paths:
Accounted exposes its bookkeeping engine as an MCP server for Claude Desktop/Code.
**MCP extension** (`extensions/general/mcp-server/`): 90+ tools covering transactions, categorization, customers/suppliers, invoices, accounts, fiscal periods, reports (trial balance, GL, BS, IS, AR/supplier ledger, VAT, KPI), reconciliation, salary runs, AGI, year-end, document upload, and loadable skills. JSON-RPC 2.0. Endpoint: `/api/extensions/ext/mcp-server/mcp`.
**MCP extension** (`extensions/general/mcp-server/`): 150+ tools (count `name: 'gnubok_` in `server.ts`; docs say "150+", never an exact number, because it drifts) covering transactions, categorization, customers/suppliers, invoices, accounts, fiscal periods, reports (trial balance, GL, BS, IS, AR/supplier ledger, VAT, KPI), reconciliation, salary runs, AGI, year-end, document upload, and loadable skills. JSON-RPC 2.0. Endpoint: `/api/extensions/ext/mcp-server/mcp`.
**OAuth 2.1** for Claude and ChatGPT connectors: `.well-known/oauth-protected-resource` + `.well-known/oauth-authorization-server` discovery; `/api/mcp-oauth/authorize`, `/token` (PKCE), `/register`. Stateless AES-256-GCM auth codes (`lib/auth/oauth-codes.ts`). Single-use via `oauth_used_codes`. Allowlist: `claude.ai/api/*`, `claude.com/api/*`, `chatgpt.com/connector/oauth/*`, `chatgpt.com/connector_platform_oauth_redirect`, `localhost`.
@@ -17,6 +17,10 @@ Accounted exposes its bookkeeping engine as an MCP server for Claude Desktop/Cod
**Tool namespaces**: internal tool ids and authorization maps remain canonical `gnubok_*`. The Accounted MCP surface is explicitly selected with `?tool_namespace=accounted`; it advertises `accounted_*` and accepts both aliases. Requests without the selector must retain the legacy server identity, catalog, and behavior.
**Lazy auth** (issue #1814, `public-tools.ts`): a client with no token may `initialize`, `ping`, list tools/prompts/resources and call the three public documentation tools (`gnubok_search_tools`, `gnubok_list_skills`, `gnubok_load_skill`), rate-limited per truncated IP. Every other request, i.e. the first company-scoped `tools/call`, answers a transport-level `401` + `WWW-Authenticate: Bearer resource_metadata="..."` from `handleMcpRequest` in `server.ts` (around line 18620). That challenge is what Claude.ai turns into its Connect card and Claude Code into `/mcp` login, and the account can be created inside it, so never answer a tokenless call with a 200 + `isError`. A public tool must read no tenant data, need no scope (absent from `TOOL_SCOPE_MAP`) and be company-independent.
**Feedback + tasks**: `gnubok_feedback` lets an agent report a missing tool, a misleading description, a wrong result or a positive signal; it emits an `agent.feedback` row to `event_log` (rate-limited 1 per 60 s per actor) and is read by the feedback-triage loop. Long-running tool calls use the MCP Tasks extension (`io.modelcontextprotocol/tasks`, `tasks.ts`): a task-capable client gets a task handle immediately and polls `tasks/get`; rows live in `mcp_tasks` (service-role writes only) so handles survive serverless instance turnover.
## Tool authoring conventions (enforced by tests)
- Every `inputSchema` must declare `additionalProperties: false` at the top level. Guarded by `extensions/general/mcp-server/__tests__/strict-schemas.test.ts`.
+1 -1
View File
@@ -72,7 +72,7 @@ npm run skills:generate # Regenerate agent_atom_registry seed after editing an
- **Events**: `lib/events/bus.ts` is a module-level singleton. Any route that emits events must call `ensureInitialized()` (`lib/init.ts`) at module level: otherwise extension handlers are never wired and events silently go nowhere.
- **Supabase clients**: browser `client.ts`, server `createClient()`, service role `createServiceClient()`, cookieless service role `createServiceClientNoCookies()` (lives in `lib/auth/api-keys.ts`; for API-key/MCP paths). Paginate with `fetchAllRows()`: PostgREST silently caps at 1000 rows.
- **Extensions**: opt-in plugins in `extensions/general/<name>/`; `extensions.config.json` is the source of truth for what's enabled. Core must run with zero extensions.
- **MCP server**: the bookkeeping engine is exposed as 100+ MCP tools (`extensions/general/mcp-server/`), authenticated by `gnubok_sk_` API keys (SHA-256, scoped, default 100 RPM per key).
- **MCP server**: the bookkeeping engine is exposed as 150+ MCP tools (`extensions/general/mcp-server/`), authenticated by `gnubok_sk_` API keys (SHA-256, scoped, default 100 RPM per key).
- **Types**: import from `@/types` (`types/index.ts`); event types in `lib/events/types.ts`.
- **User-facing errors are Swedish**: map through `lib/errors/get-error-message.ts`.
- **Cron**: hosted cron jobs live in `vercel.json`, authenticated via `verifyCronSecret()` (`lib/auth/cron.ts`).
+1
View File
@@ -1253,6 +1253,7 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-08-26] No ratchet on direct requireAuth() calls in app/api: requireAuth() is the MFA (AAL2) guard withRouteContext itself calls, and .claude/rules/api-routes.md sanctions it for routes without a company context (onboarding, account, user prefs). The 20 remaining direct callers skip request ids and the canonical envelope, not MFA; migrating them is a consistency campaign, not a security fix, so it was not folded into the bypass PR.
[2026-08-26] defer_invoice_booking (#967) now gates booking on every door, not just the dashboard: MCP send_invoice / mark_invoice_sent / create_supplier_invoice_from_inbox, v1 invoices send / mark-sent and supplier-invoices create, and the inbox convert route all checked accounting_method === 'accrual' and posted a verifikat at issue for deferred companies. All six now call booksInvoicesOnIssue() (lib/bookkeeping/booking-mode.ts), the same helper the dashboard routes use, so the setting has one meaning. No data repair attempted: vouchers already posted for deferred companies through these doors are legitimate entries and stay.
[2026-08-20] The swedish-e-invoicing skill now names Upphandlingsmyndigheten as Sweden Peppol Authority across all eight files, not just the one that was flagged: the handover completed 1 July 2026 (regeringsbeslut Fi2025/01826) and the skill was written in future tense, so a partial fix would have left the atom internally contradictory and still pointed agents at peppol@digg.se. Four digg.se URLs were repointed to their verified 301 targets on upphandlingsmyndigheten.se; the fifth, DIGG Peppol testbadd, is a hard 404 with no redirect and no successor page at the new authority, so it was replaced with the SFTI Validex verification service (https://sfti.validex.net/) rather than left dead or guessed at. Historical attributions (Q4 2025 traffic statistics, the 0007:2021006883 Peppol-ID example) deliberately still say DIGG because they were accurate when published.
[2026-08-26] MCP tool counts in docs say "150+" (connect-claude, READMEs, rules, registry entry) instead of deriving the number from the tools array: lib/docs/content/connect-claude.ts is core code and core must never import from @/extensions/ (CI builds core with zero extensions), and an exact hand-written number (90+, 100+, 120) had drifted three times already. Regenerate by counting `name: 'gnubok_` in server.ts when the order of magnitude changes.
[2026-08-26] API-key scope pickers (settings panel + OAuth consent) render from one SCOPE_GROUPS list in lib/auth/scope-catalog.ts, with MCP tool counts derived from TOOL_SCOPE_MAP at module load: the panel's hand-copied group list had drifted to 24 of 30 scopes (no articles, companies:write or reconciliation, so dashboard-minted keys could not call those tools) and every per-scope count was stale. The catalogue is a separate pure module rather than api-keys.ts itself because the panel is a client component and api-keys.ts imports crypto and the service-role client; a unit test enforces one-group-per-scope so the next scope cannot silently vanish from the pickers.
[2026-08-26] Webhook event catalogue lives in lib/webhooks/public-events.ts (grouped, with docs prose) and the fan-out handler set, the v1 create enum (so the OpenAPI spec and skills/accounted-api), and the docs page all derive from it: the enum and the docs had drifted to 24 of the 28 events the handler delivered, so the four reconciliation.* events were rejected at subscribe time. No API_V1_VERSION bump: the changelog already lists them as additive.
[2026-08-26] Removed the phantom V1_ENDPOINT_SCOPES entries GET /api/v1/openapi.yaml, GET /api/v1/companies/:companyId and GET /api/v1/companies/:companyId/events instead of building the routes: no route file, registry entry, docs, skill or test referenced them, and the new scope-registry-parity test needs the map to describe only what exists. A company-detail GET can be added later with its entry in the same PR.
+1 -1
View File
@@ -2,7 +2,7 @@
The official plugin for [Accounted](https://app.accounted.se), the open-source Swedish bookkeeping platform. Installing it gives Claude two things at once:
1. **The connection**: the Accounted MCP server (90+ bookkeeping tools, resources, and loadable skills) via OAuth. No API key needed.
1. **The connection**: the Accounted MCP server (150+ bookkeeping tools, resources, and loadable skills) via OAuth. No API key needed.
2. **The flows**: seven short workflow skills that follow the Swedish bookkeeping rhythm. Each one grounds itself in your company's live data, loads the product's Swedish accounting knowledge when it needs it, and stages every write for your approval. Nothing is ever booked without you saying yes.
## Install
+8 -3
View File
@@ -17,7 +17,7 @@ Enforced by tests in `__tests__/`: these are not style preferences, they're guar
Tool definitions (name, description, inputSchema, outputSchema, annotations) are declared as static object literals at module load: no timestamps, no UUIDs, no Date/Math.random in the definition layer. This makes the `tools/list` JSON payload byte-stable across requests, which lets agent-side prompt caches stay warm. **Do not introduce per-request non-determinism into the definitions block.** Anything time-bound or random belongs inside `execute()`.
For internal Anthropic API usage (today only `extensions/general/invoice-inbox/lib/extract-invoice-fields.ts`): annotate stable prefixes with `cache_control: { type: 'ephemeral' }` and log `usage.cache_read_input_tokens` for hit-ratio observability. The 1h TTL from the agent-native API plan (item 10) requires the direct Anthropic API; Accounted's Bedrock path defaults to a shorter TTL.
For internal Anthropic API usage (the SDK is called from `lib/ai/provider.ts` and `lib/ai/services/anthropic-family.ts`; features such as `extensions/general/invoice-inbox/lib/extract-invoice-fields.ts` go through `getAiService()` in `lib/ai` rather than the SDK directly): annotate stable prefixes with `cache_control: { type: 'ephemeral' }` and log `usage.cache_read_input_tokens` for hit-ratio observability. The 1h TTL from the agent-native API plan (item 10) requires the direct Anthropic API; Accounted's Bedrock path defaults to a shorter TTL.
## Payload-size watchdog
@@ -27,8 +27,13 @@ For internal Anthropic API usage (today only `extensions/general/invoice-inbox/l
- `server.ts`: the tools array + JSON-RPC dispatcher
- `tool-result.ts`: `withNext()`, `toToolError()` response helpers
- `resources/`: read-only `Accounted://` URIs (active company, period, recent activity, capabilities, attention items, voucher gaps, chart of accounts, VAT treatments)
- `widgets/`: inline HTML widgets (receipt-matcher, vat-review)
- `resources/`: read-only `Accounted://` URIs, registered in `resources/index.ts`: `company/current`, `period/active`, `recent-activity`, `capabilities`, `attention`, `chart-of-accounts`, `settings/vat-treatments`, `booking-templates`, `ledger/context`, `reconciliation/summary`
- `widgets/`: inline HTML widgets (receipt-matcher, vat-review, pending-operations)
- `prompts/`: slash-command-style prompts
- `skills/`: domain-knowledge skill bodies served via `gnubok_load_skill`
- `public-tools.ts`: lazy authentication (issue #1814). `ANONYMOUS_METHODS` (initialize, ping, tools/prompts/resources listing) and the three `PUBLIC_TOOLS` (`gnubok_search_tools`, `gnubok_list_skills`, `gnubok_load_skill`) answer without credentials, rate-limited per truncated IP; every other `tools/call` gets a transport-level 401 + `WWW-Authenticate` from `handleMcpRequest` in `server.ts`, which is the challenge clients turn into their Connect prompt
- `tasks.ts`: MCP Tasks extension (`io.modelcontextprotocol/tasks`): durable handles for long-running tool calls, rows in `mcp_tasks` (service-role writes only)
- `origin-guard.ts`: Origin-header validation on the Streamable HTTP endpoint (DNS-rebinding defence required by the MCP spec)
- `staging-pii-guard.ts`: refuses a plaintext personnummer in staged `pending_operations` params/preview, so every staging tool inherits the encrypt-at-staging rule
- `tool-namespace.ts`: `?tool_namespace=accounted` handling (`accounted_*` aliases for the canonical `gnubok_*` ids)
- `__tests__/`: strictness guards + per-tool coverage
+7 -4
View File
@@ -1,11 +1,14 @@
/**
* The current API version date.
*
* Echoed in every response's `meta.api_version` and accepted as the optional
* `Gnubok-Version` request header for future date-pinned breaking changes.
* Echoed in every response's `meta.api_version` and stamped as the
* `Gnubok-Version` response header (with-api-v1.ts). A `Gnubok-Version`
* REQUEST header is reserved for future date-pinned breaking changes: nothing
* reads it today, every request gets the current version.
*
* Bump this when shipping a breaking change inside v1; older versions are
* preserved as long as integrators pin to them via the header.
* Bump this only for a breaking change inside v1 (additive changes never bump
* it); that is also when request pinning gets wired so integrators can keep
* the older shape.
*/
export const API_V1_VERSION = '2026-05-12'
+13 -1
View File
@@ -43,6 +43,18 @@ The first stable release of the public REST API. Six phases of development cover
- New scopes \`reconciliation:read\`, \`reconciliation:write\`, \`reconciliation:signoff\`. The legacy \`/reconciliation/bank/*\` endpoints and their \`transactions:*\` scopes are unchanged.
- **Webhooks**: new event types \`reconciliation.matched\`, \`reconciliation.unmatched\`, \`reconciliation.signed_off\`, \`reconciliation.reopened\`. Additive: existing subscriptions are unaffected and the API version date is unchanged.
### Reports, companies, Skatteverket, customers, transactions (2026-08, additive)
Backfilled 2026-08-26 from merged PRs. Every item is additive (new endpoints, optional fields, optional filters): the API version date stays \`${API_V1_VERSION}\`.
- **Custom date ranges on reports** (#1909, 2026-08-25): \`GET /reports/income-statement\` and \`GET /reports/balance-sheet\` accept optional \`from_date\` / \`to_date\` (\`YYYY-MM-DD\`, both inside the fiscal period named by \`period_id\`, \`from_date <= to_date\`); the balance sheet also takes \`as_of\` as an alias for \`to_date\`. Omit both for the whole period, as before. These routes now reject unknown query parameters with \`400 VALIDATION_ERROR\` (\`unknown_params\` + \`allowed_params\` in details) instead of silently returning a full-period report.
- **Report PDFs** (#1909): \`GET /reports/balance-sheet/pdf\` and \`GET /reports/income-statement/pdf\` return \`application/pdf\`, byte-equivalent to the dashboard export, with the same \`period_id\` and range parameters. Scope \`reports:read\`.
- **Company creation** (#1864, 2026-08-25): \`POST /api/v1/companies\` (scope \`companies:write\`) creates a company and sets it up in one call: owner membership, BAS chart of accounts for the company form, compliance settings, the first fiscal period and the automatic tax deadlines. A VAT-registered company must send \`moms_period\`. Not idempotent, and \`Idempotency-Key\` is not honoured on this company-less route: list \`GET /api/v1/companies\` before retrying.
- **Filed VAT declarations** (#1773, 2026-08-21): \`GET /skatteverket/vat-declarations?period_type=&year=&period=\` (scope \`compliance:read\`) reads one period's momsdeklaration as Skatteverket has it on file: \`submitted\` (SKV inlämnat) and \`decided\` (SKV beslutat), each \`null\` when nothing is on file. Live read, requires an active Skatteverket connection on the company.
- **Company settings write** (#1405, 2026-08-04): \`PATCH /companies/{companyId}/settings\` (scope \`companies:write\`, risk medium, dry-run supported) updates bank details (\`bank_name\`, \`clearing_number\`, \`account_number\`, \`bankgiro\`, \`plusgiro\`, \`swish\`, \`iban\`, \`bic\`), \`contact_person\`, \`email\`, \`phone\`, \`website\` and \`invoice_email_texts\`, with the same validation as the MCP tool.
- **Customer \`personal_number\`** (#1724 2026-08-20, #1788 2026-08-21): \`personal_number\` on customer create, bulk-create, detail, PATCH and list. Accepted only for \`customer_type=individual\`, stored encrypted, always returned masked (\`********-1234\`); sending the masked form on PATCH means "leave unchanged", \`null\` clears it. A personnummer-shaped \`org_number\` on a business customer type is refused (\`400 CUSTOMER_ORG_NUMBER_IS_PERSONAL\`); on an individual it is moved into \`personal_number\` and \`org_number\` is cleared, or refused with \`400 CUSTOMER_PERSONAL_NUMBER_CONFLICT\` when it differs from a \`personal_number\` in the same body.
- **Transactions by bank account** (#1809, 2026-08-23): transaction list and detail carry \`cash_account_id\`; \`GET /transactions?cash_account_id=<uuid>\` filters to one bank account.
### Bookkeeping primitives + AP + compliance (Phase 4)
- **Suppliers + supplier-invoices** vertical (mirror of Phase 2 invoices on the AP side).
@@ -60,7 +72,7 @@ The first stable release of the public REST API. Six phases of development cover
- **Binary report**: \`GET /reports/sie-export\` (text/plain SIE4 file). Note: a SIE4 export alone does NOT satisfy BFL 7 kap archiving obligations: SIE captures account-level positions and verifikationer but lacks system documentation and behandlingshistorik. Treat SIE as a portability format (Fortnox/Visma/Bokio migration), not as a complete archive.
- **Async imports**: \`POST /imports/sie\` (multipart, 50 MB), \`POST /imports/bank\` (multipart, 10 MB, auto-format detection across 12 bank formats). Both async via \`operations\` substrate. **Post-SIE-import warning:** SIE files do NOT carry VAT codes or tax-rate-to-account mappings, AND they do NOT transfer behandlingshistorik (the source system's processing log required by BFNAR 2013:2 kap 8 §) or systemdokumentation. After importing from Fortnox / Visma / BL / SpeedLedger / Bokio you MUST manually reconfigure VAT codes (typically via \`/settings/tax-codes\`) before the first momsdeklaration; skipping this step is the most common source of incorrect VAT submissions in migrated bookkeeping. The behandlingshistorik gap must be preserved separately: under BFNAR 2013:2 kap 8 § the obligation attaches to the entire räkenskapsår, not from the import date forward. Best practice for a mid-year migration: export the source system's behandlingshistorik for the full fiscal year and archive it alongside the SIE file. Accounted starts a fresh behandlingshistorik from the import date forward; the pre-import portion of the year remains the source system's record.
### 2026-05-15 — Webhooks (Phase 6 PR-1)
### 2026-05-15: Webhooks (Phase 6 PR-1)
- **Subscriptions**: \`POST /webhooks\` (HMAC secret returned exactly once), GET list + detail, PATCH, DELETE. Per-event-type elevated scope check (\`salary_run.*\` and \`agi.generated\` require \`payroll:read\`).
- **Delivery substrate**: dispatched immediately after the event is enqueued, with a per-minute Vercel cron at \`/api/webhooks/dispatch/cron\` as the retry and sweep path. Due rows are claimed atomically via the \`claim_due_webhook_deliveries\` SQL function (\`FOR UPDATE SKIP LOCKED\`), with \`*.pg.test.ts\` coverage for the claim path and the webhook DB triggers. Exponential backoff \`1m / 5m / 30m / 2h / 12h / 24h / 48h\` (7 retries, ~87h total). HTTP 410 from receiver auto-disables the webhook.
+4 -3
View File
@@ -2,7 +2,7 @@ export const CONNECT_CLAUDE_MD = `# Connect with Claude
> Talk to your bookkeeping. Connect Accounted to Claude (claude.ai, Claude Desktop, or Claude Code) and ask questions, categorise transactions, and prepare a momsdeklaration in plain language: every write still stages for your approval first.
Accounted ships an [MCP](https://modelcontextprotocol.io) server that exposes the full bookkeeping engine (90+ tools) to any MCP client. The endpoint is:
Accounted ships an [MCP](https://modelcontextprotocol.io) server that exposes the full bookkeeping engine (150+ tools) to any MCP client. The endpoint is:
\`\`\`
https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted
@@ -20,8 +20,9 @@ Best for most users. No API key to manage: you authorise Accounted the same way
https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted&client=claude-connector
\`\`\`
_The "client=claude-connector" parameter is telemetry-only. Keep "tool_namespace=accounted": it selects the Accounted tool names._
3. Claude opens the Accounted OAuth 2.1 consent screen. Sign in and pick the company you want Claude to act on.
4. On the consent screen you grant **read-only scopes by default** (list invoices, read reports, compute VAT). Write scopes (create invoice, categorise, book vouchers, run year-end) are **listed separately and must be ticked explicitly**: leave them unchecked for a read-only review session.
If the dialog asks about authentication, choose **"Required when the server asks"** (not the auto-detected "None") and let Claude register an OAuth client automatically.
3. You do not need an Accounted account yet. The connector works as soon as it is added: the server answers the handshake and the documentation tools (search tools, list and load skills) without credentials, and the first company-scoped call opens the Accounted sign-in. A new user creates the account right there (BankID or e-mail + 2FA), no visit to the website first.
4. Sign in and pick the company you want Claude to act on. On the consent screen you grant **read-only scopes by default** (list invoices, read reports, compute VAT). Write scopes (create invoice, categorise, book vouchers, run year-end) are **listed separately and must be ticked explicitly**: leave them unchecked for a read-only review session.
5. Approve. Claude now lists the Accounted tools and you can start asking questions.
Because the consent is per-company and scoped, you can connect a read-only key for a reviewer and a separate write-enabled connection for day-to-day bookkeeping.
+5 -5
View File
@@ -36,7 +36,7 @@ When we ship a new dated version that breaks an existing shape:
1. The new version is dated forward (e.g. \`2026-08-01\`) and made the default for newly-created keys + webhooks.
2. The previous version stays available for at least **6 months** after the new version ships.
3. Deprecation appears in the [changelog](/docs/api/changelog) with the retirement date and a migration guide.
4. Three months before retirement, every response from a deprecated version stamps \`Gnubok-Deprecation: <ISO date>\` in headers.
4. Three months before retirement, responses from the deprecated version will carry a \`Gnubok-Deprecation: <ISO date>\` header. Planned alongside request pinning: no version has been deprecated yet and nothing emits the header today.
5. Calls to a retired version receive HTTP 410 with code \`API_VERSION_RETIRED\`.
We will not break a shape inside an active dated version. Additive changes (new optional response fields, new request fields with defaults, new endpoints) ship as patch updates and are always backwards-compatible.
@@ -75,7 +75,7 @@ Every state-changing endpoint (POST, PATCH, DELETE) accepts an \`Idempotency-Key
### Required vs supported
Most write endpoints **require** an Idempotency-Key — not only the obvious creates (invoices, customers, supplier-invoices, webhooks) but also journal-entry and salary-run creation, invoice \`send\`, period \`lock\`/\`close\`/\`year-end\`, bank reconciliation runs, dimension-value creation, PATCH/DELETE on customers, and more. Each endpoint's reference page states whether the key is required. Calls that omit a required key return \`400 VALIDATION_ERROR\` with field \`Idempotency-Key\`.
Most write endpoints **require** an Idempotency-Key: not only the obvious creates (invoices, customers, supplier-invoices, webhooks) but also journal-entry and salary-run creation, invoice \`send\`, period \`lock\`/\`close\`/\`year-end\`, bank reconciliation runs, dimension-value creation, PATCH/DELETE on customers, and more. Each endpoint's reference page states whether the key is required. Calls that omit a required key return \`400 VALIDATION_ERROR\` with field \`Idempotency-Key\`.
Other endpoints **support** but don't require it. Sending one is always safe.
@@ -95,11 +95,11 @@ In an agent loop, generate the key once at the *start* of an attempt and reuse i
## Dry-run
Every state-changing endpoint that supports dry-run (\`x-dry-run-supported: true\` in the OpenAPI spec) accepts \`?dry_run=true\` query param **or** \`X-Dry-Run: true\` header. The endpoint executes its full validation pipeline (Zod, business rules, period-lock checks, VAT-rate compatibility, cross-tenant guards, ...) but does NOT commit. A dry-run always returns **HTTP 200** with the \`X-Dry-Run: true\` response header — never the resource's normal success status (201, 204). The body wraps a preview, not the resource itself:
Every state-changing endpoint that supports dry-run (\`x-dry-run-supported: true\` in the OpenAPI spec) accepts \`?dry_run=true\` query param **or** \`X-Dry-Run: true\` header. The endpoint executes its full validation pipeline (Zod, business rules, period-lock checks, VAT-rate compatibility, cross-tenant guards, ...) but does NOT commit. A dry-run always returns **HTTP 200** with the \`X-Dry-Run: true\` response header, never the resource's normal success status (201, 204). The body wraps a preview, not the resource itself:
- All **local** \`validation_error\` shapes that a real commit would produce surface here (Zod, business rules, period locks, cross-tenant guards). Failures that depend on external providers do **not** surface — dry-run skips them (see below), so a VIES/BankID/Skatteverket rejection only appears on the real commit.
- All **local** \`validation_error\` shapes that a real commit would produce surface here (Zod, business rules, period locks, cross-tenant guards). Failures that depend on external providers do **not** surface: dry-run skips them (see below), so a VIES/BankID/Skatteverket rejection only appears on the real commit.
- \`data.preview\` holds the would-be record (same shape as the success response).
- For **financial** writes (invoices, journal entries, period ops, salary) the preview also carries \`staged_operation_id\`, the \`journal_lines\` that would be posted, \`account_deltas\`, and \`voucher_number_assigned_on_commit\` (a projection — the committed number can differ by one or two if another writer takes the next number first).
- For **financial** writes (invoices, journal entries, period ops, salary) the preview also carries \`staged_operation_id\`, the \`journal_lines\` that would be posted, \`account_deltas\`, and \`voucher_number_assigned_on_commit\` (a projection: the committed number can differ by one or two if another writer takes the next number first).
\`\`\`json
{
+1 -1
View File
@@ -52,7 +52,7 @@ if (rawClient && !CLIENT) {
if (!API_KEY) {
process.stderr.write(
'Error: ACCOUNTED_API_KEY is required.\n' +
'Get your API key at: https://app.accounted.se/settings?tab=api\n' +
'Get your API key at: https://app.accounted.se/settings/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' +
+2 -2
View File
@@ -5,13 +5,13 @@ should use [`accounted-mcp`](https://www.npmjs.com/package/accounted-mcp).
This package, its environment variables, and all existing API keys remain
supported.
Connect [Claude Desktop](https://claude.ai/download) (or any stdio MCP client) to your [Accounted](https://app.gnubok.se) bookkeeping account. This is a thin stdio → HTTPS bridge: it forwards JSON-RPC over stdio to the hosted Accounted MCP server, which exposes 90+ bookkeeping tools (invoices, transactions, VAT/momsdeklaration, payroll, reports, year-end).
Connect [Claude Desktop](https://claude.ai/download) (or any stdio MCP client) to your [Accounted](https://app.gnubok.se) bookkeeping account. This is a thin stdio → HTTPS bridge: it forwards JSON-RPC over stdio to the hosted Accounted MCP server, which exposes 150+ bookkeeping tools (invoices, transactions, VAT/momsdeklaration, payroll, reports, year-end).
Write tools stage a pending operation that you confirm before anything is booked: the bridge never books on its own.
## Quickstart
1. Mint an API key in the Accounted dashboard at **[/settings/api](https://app.gnubok.se/settings?tab=api)**. Use a `gnubok_sk_test_*` key against the sandbox while you evaluate; switch to `gnubok_sk_live_*` for real data. The key's scopes gate which tools are callable.
1. Mint an API key in the Accounted dashboard at **[/settings/api](https://app.gnubok.se/settings/api)**. Use a `gnubok_sk_test_*` key against the sandbox while you evaluate; switch to `gnubok_sk_live_*` for real data. The key's scopes gate which tools are callable.
2. Run the bridge with the key in the environment:
+2 -2
View File
@@ -15,7 +15,7 @@
* }
* }
*
* Get your API key at: https://app.gnubok.se/settings?tab=api
* Get your API key at: https://app.gnubok.se/settings/api
*/
const API_KEY = process.env.GNUBOK_API_KEY
@@ -33,7 +33,7 @@ if (rawClient && !CLIENT) {
if (!API_KEY) {
process.stderr.write(
'Error: GNUBOK_API_KEY is required.\n' +
'Get your API key at: https://app.gnubok.se/settings?tab=api\n' +
'Get your API key at: https://app.gnubok.se/settings/api\n' +
'\n' +
'Add it to your Claude Desktop config:\n' +
'{\n' +
+4 -4
View File
@@ -1,6 +1,6 @@
---
title: "accounted MCP-server"
description: "MCP-server för svensk dubbel bokföring. Över 120 verktyg över bokföring, fakturor, leverantörer, moms, lön och rapporter. OAuth 2.1 med PKCE och dynamisk klientregistrering. Installeras som connector i Claude.ai, Claude Desktop, Cursor och Continue."
description: "MCP-server för svensk dubbel bokföring. Över 150 verktyg över bokföring, fakturor, leverantörer, moms, lön och rapporter. OAuth 2.1 med PKCE och dynamisk klientregistrering. Installeras som connector i Claude.ai, Claude Desktop, Cursor och Continue."
slug: "gnubok-mcp"
kind: "mcp"
author: "gnubok"
@@ -8,7 +8,7 @@ status: "live"
lang: "sv"
personas: ["developer", "founder", "byra"]
publishedAt: "2026-05-12"
updatedAt: "2026-07-18"
updatedAt: "2026-08-26"
featured: true
externalUrl: "https://app.gnubok.se/api/extensions/ext/mcp-server/mcp"
repoUrl: "https://github.com/erp-mafia/accounted"
@@ -27,7 +27,7 @@ version: "1.0.1"
ogImageEyebrow: "MCP-server"
faq:
- q: "Vad är accounted MCP-server?"
a: "En HTTP-baserad MCP-server som ger en AI-agent åtkomst till en svensk huvudbok: verifikat, kunder, fakturor, moms, lön, SIE-export. OAuth 2.1 med PKCE och dynamisk klientregistrering. Över 120 verktyg över bokföring, fakturor, leverantörer, moms, lön och rapporter."
a: "En HTTP-baserad MCP-server som ger en AI-agent åtkomst till en svensk huvudbok: verifikat, kunder, fakturor, moms, lön, SIE-export. OAuth 2.1 med PKCE och dynamisk klientregistrering. Över 150 verktyg över bokföring, fakturor, leverantörer, moms, lön och rapporter."
- q: "Hur funkar OAuth-flödet?"
a: "MCP-klienten upptäcker accounteds autentiseringsserver via `.well-known/oauth-protected-resource`. Klienten registrerar sig dynamiskt (RFC 7591) på `/api/mcp-oauth/register`, öppnar `/api/mcp-oauth/authorize` i webbläsaren, du loggar in på accounted och godkänner anslutningen, klienten utbyter authorization code mot en access token via `/api/mcp-oauth/token` med PKCE S256."
- q: "Vilka klienter funkar?"
@@ -42,7 +42,7 @@ faq:
## Servern i korthet
Bryggan exponerar över 120 verktyg som en MCP-klient kan kalla. De grupperas
Bryggan exponerar över 150 verktyg som en MCP-klient kan kalla. De grupperas
ungefär så här:
| Område | Exempel-verktyg | Server-side scope |
+5 -3
View File
@@ -66,9 +66,11 @@ imports) return `202` with an operation id; poll `GET /api/v1/operations/{id}`
until `status` is `succeeded`/`failed`. The response shape is identical
whether the work ran inline or queued.
**Versioning.** Dated versions (current: see `meta.api_version`). Pin with the
`Gnubok-Version` request header; responses echo it. Additive changes ship
without a version bump; see https://app.gnubok.se/docs/api/versioning.
**Versioning.** Dated versions (current: see `meta.api_version`). Responses
carry the `Gnubok-Version` header; request pinning via a `Gnubok-Version`
request header is reserved for a future breaking change and is not read
today. Additive changes ship without a version bump; see
https://app.gnubok.se/docs/api/versioning.
**Index badges.** Every operation line below carries machine-readable
annotations from the spec: `scope:` (required key scope), `risk:` (low/medium/
+5 -3
View File
@@ -128,9 +128,11 @@ imports) return `202` with an operation id; poll `GET /api/v1/operations/{id}`
until `status` is `succeeded`/`failed`. The response shape is identical
whether the work ran inline or queued.
**Versioning.** Dated versions (current: see `meta.api_version`). Pin with the
`Gnubok-Version` request header; responses echo it. Additive changes ship
without a version bump; see https://app.gnubok.se/docs/api/versioning.
**Versioning.** Dated versions (current: see `meta.api_version`). Responses
carry the `Gnubok-Version` header; request pinning via a `Gnubok-Version`
request header is reserved for a future breaking change and is not read
today. Additive changes ship without a version bump; see
https://app.gnubok.se/docs/api/versioning.
**Index badges.** Every operation line below carries machine-readable
annotations from the spec: `scope:` (required key scope), `risk:` (low/medium/