Files
accounted/extensions/general/mcp-server
4300cb711b fix(inbox): match suppliers on VAT number, not just org-nr and name (#1630)
An extracted document auto-links to a supplier by org_number, then by an
exact (case-insensitive) name match. The extractor deliberately leaves
orgNumber null unless the document carries a real Swedish
organisationsnummer, so for every foreign supplier the name was the only
key left: "ADOBE SYSTEMS SOFTWARE IRELAND LTD" prints nothing but a
momsregistreringsnummer (IE6364992H), which the suppliers table already
stores in vat_number and which no code path looked at.

Adds vat_number as a match key between org_number and name, and collapses
the five inlined copies of the lookup into lib/suppliers/match-supplier.ts:

- invoice-inbox upload (sync and deferred worker)
- invoice-inbox PUT /items/:id/extracted-data
- MCP createDocumentInboxItem (org-nr only until now: gains VAT and name)
- MCP gnubok_set_inbox_extracted_data
- MCP gnubok_create_supplier_invoice_from_inbox, which additionally read
  supplierExt.organizationNumber, a key the extraction schema never
  writes, so its org-nr lookup could not fire at all

VAT numbers are compared on a canonical key (uppercased alphanumerics), so
formatting variants match and a prefix-less "556012579001" still matches
"SE556012579001"; two different country prefixes never do.

Also escapes LIKE metacharacters in the name lookup, so a supplier named
"100 % Solutions" is no longer a wildcard pattern.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
2026-08-17 14:52:50 +02:00
..
2026-07-15 15:53:15 +02:00
2026-07-24 15:03:50 +02:00
2026-07-24 15:03:50 +02:00
2026-07-24 15:03:50 +02:00

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.

  1. additionalProperties: false on every inputSchema. Guarded by strict-schemas.test.ts. Forces clear rejections on hallucinated fields instead of silent ignores.
  2. Descriptions ≤ 280 chars. Guarded by output-schema.test.ts. No Args: / Returns: / Examples: prose: those belong in JSON Schema. Use agent-native hints ("Use to…", "Call X first", "HIGH risk").
  3. Staged-operation envelope for write tools: outputSchema: STAGED_OPERATION_SCHEMA (server.ts). Fields: staged, risk_level, actor, message, preview, period_status?, next?. The staged: true boolean is the explicit completion signal; agents must not infer completion from prose. Do NOT introduce a parallel { success, shouldContinue, output } envelope.
  4. period_status threading: any tool that ties to a fiscal-period-bound date (categorize, mark paid, create voucher, correct/reverse entry, approve supplier invoice) passes dateForPeriodCheck to stagePendingOperation. Response then includes period_status: { period_id, status: open|locked|closed, lock_date } so widgets and agents disable writes without round-trips.
  5. Scope mapping: every new tool needs an entry in lib/auth/api-keys.ts TOOL_SCOPE_MAP. Missing entries default to deny.
  6. Tests for new write tools: add staging-gate coverage to __tests__/voucher-tools.test.ts (or a sibling) plus executor coverage to lib/pending-operations/__tests__/voucher-executors.test.ts if the tool stages a new operation_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 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)
  • prompts/: slash-command-style prompts
  • skills/: domain-knowledge skill bodies served via gnubok_load_skill
  • __tests__/: strictness guards + per-tool coverage