Files
accounted/extensions/general/mcp-server
MattssonandClaude Fable 5.1 3918ff6620 fix(customers): make country ISO-2 everywhere and check it against the customer type (#2241)
* fix(customers): make country ISO-2 everywhere and check it against the customer type (#2025, #2028)

customers.country and suppliers.country were read as ISO codes by the
periodisk sammanstallning (SKV 5740), Peppol and the provider importers but
written as English names by the customer form and the v1 API, so a correct
German customer produced GERMANY811234567 in the SKV file plus two false
warnings, and an EU customer saved with land Sverige got reverse charge with
nothing objecting until after the invoice was sent.

- lib/vat/country-codes.ts: one helper that normalises codes and the
  Swedish/English names the writers used to store, the country-vs-type
  rule (swedish_business = SE, eu_business = EU member other than SE that
  matches the VAT prefix, non_eu_business = outside the EU), and the
  reverse-charge country gate.
- Writers: customer form and supplier form get a country select; internal
  REST, v1 REST, bulk-create, MCP create/update, CSV/Excel import and the
  provider migration mapper normalise to a code and refuse unknown text;
  the consistency rule is a form error and an API 400
  (CUSTOMER_COUNTRY_MISMATCH on update). An omitted country is SE for
  Swedish types, derived from the VAT prefix for eu_business, required
  for non_eu_business.
- vat-rules.ts: getVatRules and friends take the country as a third
  argument and grant reverse charge only for an EU country other than SE;
  every invoice/sales-order/MCP call site passes customer.country.
- periodisk sammanstallning reads legacy names through the same helper.
- Migration 20260903170000: normalize_country_code() SQL twin, country_raw
  rollback column on both tables, backfill of every non-code row; unknown
  text is left as-is. pg-real test for the function.

Closes #2025, closes #2028

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5EmmndLyDCmY5NHYAvYkE

* fix(customers): keep reverse charge for defaulted-SE EU rows, gate the country rule on the fields it reads, fix build

Skeptic and CI findings on #2241, one pass:

- Migration step 4: eu_business rows whose country was null or only the old
  writer default (SE) while the VAT number names another EU member take the
  country from the prefix. The pre-2026-09 rules granted reverse charge on
  type + VIES validation alone, so these rows invoiced at 0% and would have
  flipped to 25% on the next invoice. country_raw = '' marks a null origin;
  rollback uses nullif(country_raw, '').
- countryPermitsReverseCharge refuses SE only: a VIES-validated number
  outweighs a non-EU address (Swiss company registered in DE, Monaco with a
  FR number, Northern Ireland XI).
- checkCountryConsistency: an eu_business outside the EU VAT area is
  accepted when the VAT prefix is an EU-trade registration (incl. XI);
  Monaco maps to the FR prefix.
- Internal PATCH, MCP update and the commit executor judge the country rule
  only when customer_type, country or vat_number is part of the update, so
  a contradictory legacy row can still change its email (v1 already did).
- Webshop-order customers get the order's billing country; spreadsheet
  import derives a missing country from the type and flags contradictions
  (parser row error + execute schema refine).
- Build: v1 [id] route typed the existing row through a narrowed alias
  (never) and passed messageSv/messageEn the v1 error context lacks; the
  self-billed customer projection lacked country.
- Checks: regenerated skills/accounted-api (customer example country SE).
- New parity test holds the migration's SQL name table to the TS table.
- DECISIONS.md: correct migration version and the revised rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5EmmndLyDCmY5NHYAvYkE

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 18:09:46 +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 (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

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 on the wire without bloating the default catalog. Claude.ai only calls tools present in tools/list, so a tool that a user or a skill must call directly stays in 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, 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