The bankavstämning card showed three movement sums and a red difference, leaving the user to work out what the difference consisted of. The page already knew, exactly: every krona of it is (unmatched bank rows) minus (unmatched vouchers). Verified on prod for Arcim 1930 over 2025-07-17.. 2026-08-20: 403 565,42 bank, 332 680,93 booked, 70 884,49 difference, of which -277 799,92 sits in 74 unmatched transactions and -348 684,41 in 4 unmatched vouchers, leaving exactly 0,00 unexplained. Engine: getReconciliationStatus gains unmatched_transaction_total, unmatched_gl_line_total and unexplained_difference. The residual, not the raw difference, is the figure that can mean something is wrong: a difference is expected to be large mid-year and says nothing on its own. unmatched_gl_line_total is null rather than 0 on a foreign account, whose candidate lines carry no amount in that currency, and the card falls back to the flat figures there. Also fixes the candidate fetch's window: it used the caller's raw dateFrom while both other sides were clamped to the opening-balance floor, so a window opening before the account's IB (the v1 endpoint's default, or any multi-year range) counted vouchers from a period the reconciliation deliberately drops. UI: the card becomes a bridge whose two middle rows both explain the number and navigate to the list that resolves them, above a matched/total progress rule. Three stacked paragraphs of legal prose collapse into one line plus a tooltip, keeping the amounts on screen. The permanent destructive "Ej avstämd" badge is gone: being mid-year and unreconciled is the normal state, so it marked nothing (convention 5); Avstämd is now what gets the chip. The unmatched list becomes one line per transaction (convention 4). It rendered a ~230px card per row, each with an always-open, always-empty match field: for a real backlog that is thousands of pixels of empty search boxes, and it gave the rarest action the only visible affordance while bokför and ignorera hid behind the row menu. The picker, and its ranked-candidate fetch, now run for the one row the user opens. A non-zero residual is stated factually, never in destructive red: measured over the 206 single-1930-account companies with >=10 transactions, 136 are exactly 0,00 and 63 are >=100 kr out, dominated by ledger lines the candidate RPC hides (posted/storno on 127 companies) rather than user error. Surfacing those is follow-up work. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Accounted MCP server
JSON-RPC 2.0 server exposing the Accounted bookkeeping engine to MCP clients (Claude Desktop, Claude Code, etc.). Endpoint: /api/extensions/ext/mcp-server/mcp. Add ?tool_namespace=accounted for the Accounted tool names. Requests without it retain the legacy Gnubok namespace. OAuth and stdio bridges live alongside the API surface: see app/api/mcp-oauth/, packages/accounted-mcp/, and the compatibility package in packages/gnubok-mcp/.
Tool authoring contract
Enforced by tests in __tests__/: these are not style preferences, they're guard rails.
additionalProperties: falseon everyinputSchema. Guarded bystrict-schemas.test.ts. Forces clear rejections on hallucinated fields instead of silent ignores.- Descriptions ≤ 280 chars. Guarded by
output-schema.test.ts. NoArgs:/Returns:/Examples:prose: those belong in JSON Schema. Use agent-native hints ("Use to…", "Call X first", "HIGH risk"). - Staged-operation envelope for write tools:
outputSchema: STAGED_OPERATION_SCHEMA(server.ts). Fields:staged, risk_level, actor, message, preview, period_status?, next?. Thestaged: trueboolean is the explicit completion signal; agents must not infer completion from prose. Do NOT introduce a parallel{ success, shouldContinue, output }envelope. period_statusthreading: any tool that ties to a fiscal-period-bound date (categorize, mark paid, create voucher, correct/reverse entry, approve supplier invoice) passesdateForPeriodChecktostagePendingOperation. Response then includesperiod_status: { period_id, status: open|locked|closed, lock_date }so widgets and agents disable writes without round-trips.- Scope mapping: every new tool needs an entry in
lib/auth/api-keys.tsTOOL_SCOPE_MAP. Missing entries default to deny. - Tests for new write tools: add staging-gate coverage to
__tests__/voucher-tools.test.ts(or a sibling) plus executor coverage tolib/pending-operations/__tests__/voucher-executors.test.tsif the tool stages a newoperation_type.
Determinism / cache stability
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.
Payload-size watchdog
payload-size.bench.test.ts enforces a tools/list JSON payload ceiling. If the test fires, the right answer is rarely "raise the ceiling". Instead, trim descriptions or set specialized wide tools to catalogVisibility: 'search'. Those tools remain discoverable with full schemas through gnubok_search_tools and callable through tools/call without bloating the default catalog.
Where things live
server.ts: the tools array + JSON-RPC dispatchertool-result.ts:withNext(),toToolError()response helpersresources/: read-onlyAccounted://URIs (active company, period, recent activity, capabilities, attention items, voucher gaps, chart of accounts, VAT treatments)widgets/: inline HTML widgets (receipt-matcher, vat-review)prompts/: slash-command-style promptsskills/: domain-knowledge skill bodies served viagnubok_load_skill__tests__/: strictness guards + per-tool coverage