Files
accounted/extensions/general/mcp-server/skills/bank-reconciliation.ts
T
Jakob WennbergandClaude Opus 4.8 43007fa869 feat(mcp): self-describing agent surface — staging _meta, company identity, clean skill summaries (#775)
* feat(mcp): make the agent surface self-describing (staging _meta, company identity, clean summaries)

A pass over the MCP server's agent-facing surface so an agent can act
correctly without parsing description prose:

- Machine-readable staging contract: deriveToolMeta() attaches _meta to
  tools/list (and search detail=full) — { requires_approval, approve_tool,
  preflight? } — keyed off the STAGED_OPERATION_SCHEMA output schema. Literal
  _meta (e.g. UI widget hints) wins on collision. TOOL_PREFLIGHT_MAP names the
  read-only pre-flight for the few writes that have one (year-end readiness,
  VAT validate, depreciation proposal). Guarded by staging-meta.test.ts.
- Company identity in gnubok_get_agent_briefing: returns a `company` block
  (id, name, org_number, entity_type, accounting_method) so the agent can
  confirm WHICH entity it operates on and pick the right settlement account
  (accrual = credit 1510; cash = debit 19xx) before any write. Best-effort —
  a missing row never blocks the briefing. Covered by agent-briefing.test.ts.
- toSummary(): trims the long, keyword-stuffed SKILL.md frontmatter into clean
  one-liners for gnubok_list_skills / gnubok_get_agent_briefing so the client
  never truncates one mid-sentence; full bodies stay in gnubok_load_skill.
  Covered by to-summary.test.ts.
- bank-reconciliation skill: a match/link decision tree (what you have x
  whether a verifikat exists) and kontant- vs faktureringsmetoden settlement
  accounts.
- Prose/description clarifications: "Stages"/"Stages for approval" on the
  link tools; propose_dispositioner/accruals note there is no dedicated MCP
  poster; server-info documents _meta and the legacy gnubok_ tool prefix.

All 34 touched MCP tests pass. Merged cleanly on top of #759/#760 (server.ts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(mcp): make accounting_method description state the full settlement posting

Review (swedish-accounting-compliance): the agent-briefing schema described
accrual as "credit 1510 on payment", which reads as a one-sided entry. Spell
out both sides (payment debits 19xx AND credits 1510) so an agent can't infer a
single-leg posting that violates BFL 5 kap double-entry. Mirrors the precision
already in the bank-reconciliation skill body. Payload-size guard still passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:42:12 +02:00

143 lines
8.5 KiB
TypeScript

import type { Skill } from './types'
const body = `# Bank Reconciliation — Accounted
Reconcile the company's bank statements against the bookkeeping ledger so that the cash position in the books matches the bank. Run at month-end and before VAT close.
## When to use
- "Stäm av banken"
- "Reconcile the bank account"
- "Why doesn't my 1930 balance match the bank?"
- Mid-month spot-check before tax filings
## Choosing the right matching/linking tool
Accounted has several reconciliation tools because the *right* one depends on what
you already have in hand. Decide along two axes — **what you're connecting** and
**whether a verifikat already exists** — then pick:
| You have… | …and the entry is | Use |
|---|---|---|
| One bank tx + one customer invoice | not yet booked | \`gnubok_match_transaction_to_invoice\` |
| One bank receipt covering many invoices (customer **or** supplier) | not yet booked | \`gnubok_match_batch_allocate\` (samlingsbetalning, BFL 5 kap 6§) |
| A whole period of unmatched income to clear | not yet booked | \`gnubok_auto_match_period\` (run \`dry_run=true\` first) |
| One bank tx whose affärshändelse you already posted manually | already booked | \`gnubok_link_transaction_to_journal_entry\` |
| A customer invoice you know is paid, payment already in a verifikat | already booked | \`gnubok_find_voucher_candidates_for_invoice\` → \`gnubok_link_invoice_to_voucher\` |
| A supplier invoice paid, payment already in a verifikat | already booked | \`gnubok_find_voucher_candidates_for_supplier_invoice\` → \`gnubok_link_supplier_invoice_to_voucher\` |
| A receipt/document to file against a tx (no new bokföring) | n/a | \`gnubok_attach_document_to_transaction\` |
| Income you're sure is paid but with no bank line to point at | not yet booked | \`gnubok_mark_invoice_as_paid\` |
Rule of thumb: **match_\*** creates the payment bokföring; **link_\*** attaches to
bokföring that already exists (no new verifikat). Every one of these stages a
pending operation — the user approves before anything posts.
### Kontantmetoden vs faktureringsmetoden
The settlement posting differs by the company's \`accounting_method\` (read it from
\`gnubok_get_agent_briefing\` — \`accrual\` = faktureringsmetoden, \`cash\` =
kontantmetoden; null defaults to accrual):
- **Faktureringsmetoden (accrual):** revenue was booked at invoice time against
kundfordran **1510**. Payment **credits 1510** and debits the bank (19xx). The
link/match tools settle 1510.
- **Kontantmetoden (cash):** no receivable was raised at invoice time. Payment
**debits 19xx** and books the revenue + moms now.
You usually don't pass the method explicitly — the tools resolve it from company
settings — but knowing it lets you sanity-check the staged preview's accounts
before approving (1510 movement under accrual; revenue/moms under cash).
## Workflow
### Step 1 — Pull the reconciliation status
\`gnubok_get_reconciliation_status\` returns the current state per bank account:
matched count, unmatched count, latest bank balance, and ledger balance for the
asset account (typically 1930). If \`unmatched_count = 0\` and the balances agree,
the account is reconciled — skip to Step 5.
### Step 2 — Categorize the uncategorized side
Unmatched usually means *bank rows without journal entries* (incoming PSD2
transactions) AND *journal rows without bank lines* (manually-posted entries
the agent might already have created via \`gnubok_create_voucher\`).
For the bank-side gap:
1. \`gnubok_list_uncategorized_transactions(limit=20)\`. Page through if needed.
2. For each, decide: is this an income payment for a known invoice, or an expense?
3. **Income** that matches an open invoice: \`gnubok_match_transaction_to_invoice\` — keeps AR clean.
4. **Income** without a matching invoice (refund, deposit, owner contribution): \`gnubok_categorize_transaction\` with the appropriate category.
5. **Expense**: \`gnubok_suggest_categories\` first (uses counterparty templates + history) — accept the top suggestion if confidence is high. Otherwise pick from the category list manually.
6. **Owner draw / private withdrawal** (EF only): category \`private\` posts to 2013.
Each call stages a pending operation — the user approves in the web app.
### Step 3 — Resolve duplicates / dead entries
If the bank shows a transaction that was already booked (e.g. manually entered via \`gnubok_create_voucher\`):
- \`gnubok_match_transaction_to_invoice\` won't help here.
- The cleanest path: reverse the manual entry via \`gnubok_reverse_journal_entry\` (storno) and then categorize the bank transaction normally so the trail is "bank → ledger" rather than "ledger → bank → orphan".
If the ledger shows a phantom entry with no bank counterpart (a payment that
was never actually sent), it must be reversed: \`gnubok_reverse_journal_entry\`.
### Step 4 — Re-check status
\`gnubok_get_reconciliation_status\` should now report \`unmatched_count = 0\`
and matching balances. If the balances still disagree:
- A historical opening balance might be wrong. Compare \`gnubok_get_trial_balance\` for the period start against the bank's opening statement.
- An entry might sit in a closed period that wasn't fully reconciled before locking. Check \`gnubok_list_fiscal_periods\` and walk backwards.
- FX accounts (non-SEK) need revaluation before period close: \`gnubok_run_currency_revaluation\`.
### Step 5 — Document the reconciliation
For each fiscal period, Accounted stores the reconciliation state automatically.
For a printed audit trail (BFL 8 kap), generate the supplier ledger and AR
ledger after reconciliation:
- \`gnubok_get_ar_ledger\` — open customer balances should match unpaid invoices
- \`gnubok_get_supplier_ledger\` — open supplier balances should match unpaid leverantörsfakturor
## Critical rules
- **Never delete a bank transaction** — even if it looks wrong. The PSD2 feed is the source of truth. If the bank made a mistake, the bank reverses it via a new transaction.
- **Never edit a posted entry to "make it match"** — use \`gnubok_correct_entry\` or \`gnubok_reverse_journal_entry\`. BFL 5 kap 5 § forbids in-place edits.
- **Reconcile BEFORE locking the period.** Lock-period sealed-off audit can't be re-opened cheaply (it requires \`gnubok_unlock_period\` + storno + relock).
- **FX accounts must be revalued** at period close. Pre-revaluation reconciliation can show a phantom mismatch that disappears after revaluation — don't chase it.
## Common errors
- *"Balances disagree by exactly N"* — usually a single rounding entry at year-end (öresavrundning, 3741/7741) wasn't booked. Verify with \`gnubok_get_general_ledger\` filtered to the rounding accounts.
- *"Unmatched count is 0 but balances disagree"* — opening balance issue. Check the previous period's UB matches this period's IB via \`gnubok_get_trial_balance(period=prev)\` vs \`gnubok_get_trial_balance(period=current, opening=true)\`.
- *"Same transaction shows twice"* — either two PSD2 feeds (manual + Enable Banking) imported the same row, or the user manually created a voucher AND the bank imported the row. Reverse the duplicate via \`gnubok_reverse_journal_entry\`.
## Tools
- \`gnubok_get_reconciliation_status\` (single source of truth — call first and last)
- \`gnubok_list_uncategorized_transactions\`
- \`gnubok_suggest_categories\`
- \`gnubok_categorize_transaction\`
- \`gnubok_match_transaction_to_invoice\`, \`gnubok_match_batch_allocate\` (one receipt → many invoices)
- \`gnubok_auto_match_period\` (bulk matcher with confidence thresholds — use for big backlogs)
- \`gnubok_link_transaction_to_journal_entry\`, \`gnubok_link_invoice_to_voucher\`, \`gnubok_link_supplier_invoice_to_voucher\` (attach to an existing verifikat — no new bokföring)
- \`gnubok_find_voucher_candidates_for_invoice\`, \`gnubok_find_voucher_candidates_for_supplier_invoice\` (read-only — run before the link_\* tools)
- \`gnubok_attach_document_to_transaction\` (file a receipt against a tx)
- \`gnubok_reverse_journal_entry\` (storno)
- \`gnubok_run_currency_revaluation\` (FX accounts only)
- \`gnubok_get_trial_balance\`, \`gnubok_get_ar_ledger\`, \`gnubok_get_supplier_ledger\` (verification)
`
export const bankReconciliationSkill: Skill = {
slug: 'bank-reconciliation',
name: 'Bank Reconciliation',
summary: 'Stämma av banken: pick the right match/link tool, categorize PSD2 rows, handle kontant- vs faktureringsmetoden, resolve duplicates, verify with ledger reports.',
tags: ['monthly', 'reconciliation', 'bank', 'verification', 'matching'],
body,
tier: 'workflow',
applicability: { entity_type: 'both' },
}