fix(mcp): surface BFL 5 kap 6 § underlag requirements in voucher and transaction tooling (#1844)

* fix(mcp): surface BFL 5 kap 6 § underlag requirements in voucher and transaction tooling

Addresses a user report that the MCP surface treats document anchoring as
optional convenience while the law treats it as mandatory:

- create_voucher: description and inbox_item_id reframed as the compliant
  path for received handlingar; staging without a document now carries an
  advisory compliance_warning in the preview and a WARNING in the message
  (never blocks: IB/migration vouchers legitimately lack a kvitto).
- High-risk approval guidance now tells the agent to surface any preview
  compliance_warning alongside the 5 kap 5 § irreversibility.
- Approval-flow copy is scope-aware: staging responses and
  list_pending_operations explain that keys without
  pending_operations:approve (SoD) approve at /pending instead of hunting
  for a tool their catalog hides.
- categorize/create/bulk_book transactions: state that a transaction models
  a cash-account movement and point cashless events (privat utlagg) at
  create_voucher instead of silently fabricating a bank line.
- link_document_to_voucher: signposts the pre-posting inbox path.
- /pending VoucherPreview shows an attn line when document_attached=false.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): entity-neutral utlagg hint, coherent no-document envelope, real attn styling

Addresses the skeptic refutations on da811a58f:

- categorize_transaction: drop the 6540/2893 example; 2893 is AB-only
  (EF books egen insattning 2013/2018 per category-mapping.ts) and a
  static description cannot know the entity type.
- create_voucher: when inbox_item_id is supplied but the item has no
  stored document (document_id NULL, ON DELETE SET NULL), the will-text
  no longer claims an OCR attach and the compliance warning gets an
  inbox variant instead of a dead-end 'restage with inbox_item_id'.
  Pinned by a new test.
- /pending VoucherPreview: use the AttnLine component (the bare 'attn'
  class does not exist) and conditional wording so IB/internal vouchers
  are not falsely flagged under BFL 5 kap 6 \u00a7.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): exempt IB vouchers from the underlag warning; UI mirrors the staged warning

Swedish accounting review round 2: the warning keyed off document_attached
alone, so migrated IB entries (and the /pending card for any old no-document
row) were falsely flagged under BFL 5 kap 6 \u00a7. Staging now skips the
complianceNote when is_opening_balance=true, and VoucherPreview gates on the
staged compliance_warning instead of overloading document_attached, so the
server owns the policy in one place. Pinned by a new IB test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): trim two descriptions to fit the tools/list payload ceiling after merging main

PR #1846 spent the 59,950 headroom; the merged state crossed by 7 tokens.
Dropped the redundant filter enumeration from list_pending_operations (the
schema documents the filters) and the dims-bags aside from
bulk_book_transactions (the lines schema documents the bags).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-24 16:17:55 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent d2251736a2
commit 4d6dd68df4
4 changed files with 190 additions and 16 deletions
+1
View File
@@ -1182,6 +1182,7 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-08-23] Avstämning page (PR 3) ships without the period picker, the manual two-pane match mode and the sign-off button: the page renders the approved 'Vald riktning' layout (rail + tiles + bridge + actions + banded table) over the PR 2 dashboard routes only, so that it is verifiable on its own; period + sign-off arrive together in PR 4 (both are period-bound), manual N:M matching with residual booking in PR 5. Bank accounts get the same generic body plus links to the existing bank view for the matcher run rather than embedding the 1900-line BankReconciliationView: one body for every account kind is the point of the page, and embedding would have doubled the header.
[2026-08-23] Reconciliation sign-off (PR 4) is an append-only attestation table (account_reconciliations) with a reopen stamp, not a flag on the account: who signed what through which date, with the numbers as they stood, is the thing an auditor and the Hem row read, so it must survive a later change of mind. Sign-off is refused with an unexplained difference unless forced with a note (the note is what the next reader sees). Separate scope reconciliation:signoff (write is not enough): an integration that links rows should not be able to attest. The worklist category reconciliation_due is gated on adoption (zero until the company has signed anything off) so the nudge reaches the people who reconcile monthly without becoming a new chore for everyone. Webhook events added additively without bumping API_V1_VERSION: the dated version is reserved for breaking changes; a new event type breaks no existing subscriber.
[2026-08-23] Reconciliation agent surfaces (PR 5): the Hem notice for a skattekonto that disagrees with the ledger reads a summary the sync persists (extension_data skattekonto_reconciliation_latest) instead of recomputing the bridge on every render; the same persisted summary feeds nothing else yet. The attention resource's reconciliation_due category and the Hem row share lib/worklist countReconciliationDue (one predicate). Manual N:M matching with residual booking, dropping the local MatchDialog on /skattekonto, restyling the bank view and eval scenarios are deferred to PR 6: they need the two-pane UI and a visual pass, and none of the agent surfaces depend on them. The skattekonto sync cron now orders eligible companies by stalest sync before its 50-per-run cap (never-synced first) instead of raising the cap: a fixed order plus a cap starved the tail.
[2026-08-24] MCP underlag warnings are advisory text, not staging blocks: soft guards must stay bypassable and IB/migration vouchers legitimately lack a kvitto; approve-scope fallback copy is static because stagePendingOperation has no access to key scopes and plumbing them through every staging tool was not worth the diff.
[2026-08-24] Inline matcher (PR 6a) applies through the existing /api/reconciliation/bank/run intersection guard rather than new endpoints: the page renders the dry-run pairs and applies per row or strong-only (floor 0.85, re-enforced server-side), so a stale preview can never link a pair the fresh run would not. The old bank view keeps living until 6b (manual N:M + residual booking) reaches parity; only its matcher trips became unnecessary today.
[2026-08-24] vat_amount (categorize/bulk_book) is transaction-currency, converted to SEK at booking: the validation bound already read it in transaction currency (the underlag's denomination), and the gross line already converts through resolveSekAmount, so converting the VAT the same way was the only coherent option. Documenting it as SEK instead (the reporter's first suggestion, feedback seq 254607) would force agents to pre-convert with a settlement rate they cannot see.
[2026-08-24] Declared currency/voucher_series nullable in three MCP listing schemas on column-nullability alone (no traced null producer): loosening an output schema can only stop false validation failures, never cause one, and legacy rows predate the columns' defaults. Declined (for now) a full Ajv execute-vs-schema round-trip harness in output-schema.test.ts: right long-term answer to this bug class, but a session-sized project of its own; the audit's seven confirmed sites are pinned by a targeted declaration test instead.
@@ -9,6 +9,7 @@
import { Fragment, createContext, useContext } from 'react'
import { cn, formatCurrency } from '@/lib/utils'
import { AttnLine } from '@/components/ui/attn-line'
import { VTH_CLASS, VTD_CLASS } from '@/components/ui/dry-table'
import type { PendingOperation } from '@/types'
import { AttachDocumentPreview } from '@/components/bookkeeping/AttachDocumentPreview'
@@ -295,9 +296,24 @@ function VoucherPreview({ data }: { data: Record<string, unknown> }) {
const lines = (data.lines as VoucherLine[]) || []
const totalDebit = data.total_debit as number | undefined
const totalCredit = data.total_credit as number | undefined
// Advisory, mirrors the MCP staging warning: a verifikat for a received
// handling must carry the handling itself (BFL 5 kap 6 §). Gated on the
// staged compliance_warning, not on document_attached: the server decides
// when the warning applies (IB entries are exempt there), so this stays a
// mirror instead of a second, looser policy. Older ops without the field
// render unchanged. The wording stays conditional ("om ... avser en
// mottagen handling"): internal entries (accruals, FX) legitimately lack
// a kvitto, and flagging them as deficient would be wrong.
const missingUnderlag = typeof data.compliance_warning === 'string'
return (
<div className="space-y-3 text-sm">
{missingUnderlag && (
<AttnLine>
Underlag saknas: om verifikatet avser en mottagen handling ska handlingen användas som
verifikation (BFL 5 kap 6 §).
</AttnLine>
)}
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
<span className="text-muted-foreground">Datum</span>
<span className="font-mono">{String(data.entry_date ?? '')}</span>
@@ -300,13 +300,21 @@ describe('gnubok_create_voucher: staging gates', () => {
'company-1',
'user-1',
supabase as never,
)) as { staged: boolean; operation_id?: string; preview: Record<string, unknown> }
)) as { staged: boolean; operation_id?: string; message: string; preview: Record<string, unknown> }
expect(result.staged).toBe(true)
expect(result.operation_id).toBe('op-staged')
expect(result.preview.total_debit).toBe(250)
expect(result.preview.total_credit).toBe(250)
// No document attached: the advisory BFL 5 kap 6§ warning must reach both
// the agent (message) and the /pending approval card (preview) without
// blocking the staging itself.
expect(result.preview.document_attached).toBe(false)
expect(result.preview.compliance_warning).toMatch(/BFL 5 kap 6§/)
expect(result.preview.compliance_warning).toMatch(/underlag/i)
expect(result.message).toMatch(/WARNING:.*underlag/i)
// Critical: the staged pending_operations row must NOT carry source_type.
// The executor always hardcodes 'manual'. Look at the insert call.
const insertCalls = (supabase.from as ReturnType<typeof vi.fn>).mock.calls
@@ -376,6 +384,112 @@ describe('gnubok_create_voucher: staging gates', () => {
expect(result.preview.inbox_item_id).toBe('inbox-1')
expect(result.preview.document_attached).toBe(true)
expect(result.preview.will).toMatch(/link the inbox item/i)
// Document attached: the missing-underlag warning must NOT appear.
expect(result.preview.compliance_warning).toBeUndefined()
})
it('inbox item without a stored document: no attach claim, inbox-variant warning', async () => {
const { supabase, enqueue } = createQueuedMockSupabase()
enqueue({
data: {
id: 'fp-1',
is_closed: false,
period_start: '2026-01-01',
period_end: '2026-12-31',
name: '2026',
},
error: null,
})
enqueue({
data: [
{ account_number: '5410', account_name: 'Förbrukningsinventarier', is_active: true },
{ account_number: '1930', account_name: 'Företagskonto', is_active: true },
],
error: null,
})
enqueue({
data: {
id: 'inbox-nodoc',
document_id: null, // ingested without attachment, or ON DELETE SET NULL
created_journal_entry_id: null,
created_supplier_invoice_id: null,
},
error: null,
})
enqueue({ data: null, error: null }) // resolvePeriodStatusForDate layer 1
enqueue({ data: null, error: null }) // resolvePeriodStatusForDate layer 2
enqueue({ data: { id: 'op-nodoc' }, error: null }) // pending_operations insert
const result = (await createVoucher.execute(
{
entry_date: '2026-05-12',
description: 'Inbox item utan dokument',
fiscal_period_id: 'fp-1',
inbox_item_id: 'inbox-nodoc',
lines: [
{ account_number: '5410', debit_amount: 250, credit_amount: 0 },
{ account_number: '1930', debit_amount: 0, credit_amount: 250 },
],
},
'company-1',
'user-1',
supabase as never,
)) as { staged: boolean; preview: Record<string, unknown> }
expect(result.staged).toBe(true)
expect(result.preview.document_attached).toBe(false)
// The envelope must not contradict itself: no "attach the OCR document"
// claim, and the warning must not tell the agent to restage with the
// inbox_item_id it already supplied.
expect(result.preview.will).toMatch(/link the inbox item/i)
expect(result.preview.will).not.toMatch(/attach the OCR document/i)
expect(result.preview.compliance_warning).toMatch(/no stored document/i)
expect(result.preview.compliance_warning).not.toMatch(/restage with inbox_item_id/i)
})
it('is_opening_balance: no underlag warning (IB legitimately lacks a kvitto)', async () => {
const { supabase, enqueue } = createQueuedMockSupabase()
enqueue({
data: {
id: 'fp-1',
is_closed: false,
period_start: '2026-01-01',
period_end: '2026-12-31',
name: '2026',
},
error: null,
})
enqueue({
data: [
{ account_number: '1930', account_name: 'Företagskonto', is_active: true },
{ account_number: '2081', account_name: 'Aktiekapital', is_active: true },
],
error: null,
})
enqueue({ data: null, error: null }) // resolvePeriodStatusForDate layer 1
enqueue({ data: null, error: null }) // resolvePeriodStatusForDate layer 2
enqueue({ data: { id: 'op-ib' }, error: null }) // pending_operations insert
const result = (await createVoucher.execute(
{
entry_date: '2026-01-01',
description: 'Ingående balans',
fiscal_period_id: 'fp-1',
is_opening_balance: true,
lines: [
{ account_number: '1930', debit_amount: 25000, credit_amount: 0 },
{ account_number: '2081', debit_amount: 0, credit_amount: 25000 },
],
},
'company-1',
'user-1',
supabase as never,
)) as { staged: boolean; message: string; preview: Record<string, unknown> }
expect(result.staged).toBe(true)
expect(result.preview.document_attached).toBe(false)
expect(result.preview.compliance_warning).toBeUndefined()
expect(result.message).not.toMatch(/WARNING/)
})
it('rejects when inbox_item_id is already booked as a journal entry', async () => {
+58 -15
View File
@@ -688,13 +688,27 @@ interface StageOptions {
* the DB triggers remain the authoritative gate.
*/
dateForPeriodCheck?: string
/**
* Advisory compliance warning. Appended to the staged message and echoed as
* preview.compliance_warning, which persists into preview_data so the
* /pending approval card carries the same warning. Never blocks: the DB
* triggers and the approver stay authoritative.
*/
complianceNote?: string
}
// Keys that can stage typically lack pending_operations:approve (segregation
// of duties, see STAGING_SCOPES in lib/auth/api-keys.ts), so the approve tool
// is filtered out of their tools/list. Every staging response names the web
// fallback so agents on such keys don't hunt for a tool they cannot see.
const APPROVE_SCOPE_FALLBACK =
'If that tool is missing from your catalog, this API key lacks the pending_operations:approve scope: the user approves at /pending instead.'
function buildApprovalGuidance(operationId: string, riskLevel: 'low' | 'medium' | 'high'): string {
if (riskLevel === 'high') {
return `This is an irreversible posting under BFL 5 kap 5§: surface the irreversibility implications to the user and obtain an explicit acknowledgment before committing. Once the user has acknowledged, call gnubok_approve_pending_operation with operation_id="${operationId}" and confirmed=true.`
return `This is an irreversible posting under BFL 5 kap 5§: surface the irreversibility implications, and any compliance_warning in the preview, to the user and obtain an explicit acknowledgment before committing. Once the user has acknowledged, call gnubok_approve_pending_operation with operation_id="${operationId}" and confirmed=true. ${APPROVE_SCOPE_FALLBACK}`
}
return `When the user authorises, call gnubok_approve_pending_operation with operation_id="${operationId}".`
return `When the user authorises, call gnubok_approve_pending_operation with operation_id="${operationId}". ${APPROVE_SCOPE_FALLBACK}`
}
async function stagePendingOperation(
@@ -727,6 +741,13 @@ async function stagePendingOperation(
assertNoPlaintextPersonnummer(params, 'params')
assertNoPlaintextPersonnummer(previewData, 'preview_data')
// Fold the advisory compliance note into the preview so the agent response
// and the persisted preview_data (rendered by the /pending approval card)
// carry the same warning.
if (options.complianceNote) {
previewData = { ...previewData, compliance_warning: options.complianceNote }
}
// params-aware: create/update_recurring_schedule escalate to 'high' when
// params.auto_send === true (standing outbound email with no per-send
// approval, same side-effect that puts one-off send_invoice at 'high').
@@ -771,7 +792,7 @@ async function stagePendingOperation(
dry_run: true,
risk_level: riskLevel,
actor,
message: `Dry run: would stage "${operationType}" (risk: ${riskLevel}). No changes made.`,
message: `Dry run: would stage "${operationType}" (risk: ${riskLevel}). No changes made.${options.complianceNote ? ` WARNING: ${options.complianceNote}` : ''}`,
preview: previewData,
...(periodStatus ? { period_status: periodStatus } : {}),
...(next ? { next: addCompanyToNextHint(next, companyId) as NextActionHint } : {}),
@@ -842,7 +863,7 @@ async function stagePendingOperation(
operation_id: data.id,
risk_level: riskLevel,
actor,
message: `Staged as pending_operation ${data.id} (risk: ${riskLevel}). ${buildApprovalGuidance(data.id, riskLevel)} The user can also approve at /pending in the ${branding} web app.`,
message: `Staged as pending_operation ${data.id} (risk: ${riskLevel}). ${buildApprovalGuidance(data.id, riskLevel)} The user can also approve at /pending in the ${branding} web app.${options.complianceNote ? ` WARNING: ${options.complianceNote}` : ''}`,
approve: {
tool: 'gnubok_approve_pending_operation',
args: { operation_id: data.id, company_id: companyId } as Record<string, unknown>,
@@ -4065,7 +4086,7 @@ export const tools: McpTool[] = [
{
name: 'gnubok_create_transactions',
title: 'Create Bank Transactions',
description: 'Stage one or more transactions for the user to approve. Each creates a separate pending operation; commit each via gnubok_approve_pending_operation. Use for ingesting external rows (Airtable, CSV). Max 10.',
description: 'Stage bank/cash-account transactions; each becomes a pending operation. For external rows (Airtable, CSV); max 10. A transaction models a cash-account movement: for cashless events (privat utlägg) use gnubok_create_voucher.',
outputSchema: {
type: 'object',
additionalProperties: false,
@@ -4458,7 +4479,7 @@ export const tools: McpTool[] = [
{
name: 'gnubok_categorize_transaction',
title: 'Categorize Bank Transaction',
description: 'Categorize a bank transaction. Stages the verifikat: cost line NET of moms, bank line gross; dimensions bag tags the cost line. account_override books the business side on any active kontoplan account (custom incl.). Commit via gnubok_approve_pending_operation.',
description: 'Categorize a bank transaction. Stages the verifikat: cost line NET of moms, bank line gross (always the tx\'s cash account). account_override books the business side on any active account. Cashless events (privat utlägg): gnubok_create_voucher.',
inputSchema: {
type: 'object',
additionalProperties: false,
@@ -8835,7 +8856,7 @@ export const tools: McpTool[] = [
{
name: 'gnubok_bulk_book_transactions',
title: 'Bulk-Book Transactions',
description: 'Bulk-book N bank txs on the same date into 1 samlingsverifikat (BFL 5 kap 6§). Either link N txs to an existing posted verifikat, or create a new verifikat from caller lines (accept dims bags). All txs share date + direction. Stages.',
description: 'Bulk-book N bank txs (same date, same direction) into 1 samlingsverifikat (BFL 5 kap 6§). Link txs to an existing posted verifikat, or create one from caller lines. Each tx posts its cash-account line. Stages.',
inputSchema: {
type: 'object',
additionalProperties: false,
@@ -11342,7 +11363,7 @@ export const tools: McpTool[] = [
{
name: 'gnubok_link_document_to_voucher',
title: 'Link Document to Voucher',
description: 'Stage linking a document to a posted verifikation. Use for imported/manual vouchers with no bank-tx row. Call gnubok_list_verifikat_without_documents first to find targets. Stages for approval.',
description: 'Stage linking a document to an already-POSTED verifikation (no bank-tx row). For an unbooked handling prefer gnubok_create_voucher with inbox_item_id (BFL 5 kap 6§). Call gnubok_list_verifikat_without_documents for targets.',
inputSchema: {
type: 'object',
additionalProperties: false,
@@ -15508,7 +15529,7 @@ export const tools: McpTool[] = [
{
name: 'gnubok_create_voucher',
title: 'Create Manual Voucher (Verifikation)',
description: 'Stage a manual verifikation with arbitrary balanced lines: capitalization (1010), accruals, FX adjustments, rättelser outside categorize_transaction. Lines accept dimensions bags {sie_dim_no: code or name}. Pass inbox_item_id to book a kvitto direct. HIGH risk.',
description: 'Stage a manual verifikation with balanced lines: capitalization, accruals, FX, rättelser, IB. For a received handling pass inbox_item_id: the document becomes the verifikation (BFL 5 kap 6§); a filename in notes is not underlag. HIGH risk.',
inputSchema: {
type: 'object',
additionalProperties: false,
@@ -15524,7 +15545,7 @@ export const tools: McpTool[] = [
description: 'Dimension tags {sie_dim_no: kod eller namn}, e.g. {"6":"P001"}, applied to every line not setting the key itself. Unknown values are rejected: never auto-created.',
},
is_opening_balance: { type: 'boolean', description: 'Set true ONLY for a migrated ingående balans (IB). Marks the entry source_type=opening_balance so bank reconciliation excludes it from period movement. Requires every line to be a balance-sheet account (class 1/2) and entry_date = fiscal period start, else rejected. Defaults false.' },
inbox_item_id: { type: 'string', description: 'Optional inbox item UUID to book directly. On confirm, the inbox item is linked to the new verifikat and its OCR document is attached to the journal entry. Fails if the inbox item is already booked (as voucher) or converted (to supplier invoice).' },
inbox_item_id: { type: 'string', description: 'Inbox item UUID to book directly. For a received handling this is the BFL 5 kap 6§ path: the document becomes the verifikation. On confirm the item and its document link to the new verifikat. Not in inbox? gnubok_upload_document first. Fails if already booked/converted.' },
lines: {
type: 'array',
description: 'At least 2 balanced lines. sum(debit_amount) === sum(credit_amount), both > 0.',
@@ -15775,16 +15796,38 @@ export const tools: McpTool[] = [
...(dimensionResolutions.length > 0 ? { dimension_resolutions: dimensionResolutions } : {}),
inbox_item_id: inboxItemId,
document_attached: Boolean(inboxDocumentId),
will: inboxItemId
// The document-attach claim keys off inboxDocumentId, not
// inboxItemId: an inbox item whose document was never stored (or
// was deleted, ON DELETE SET NULL) links but attaches nothing.
will: inboxDocumentId
? 'create a posted journal entry with a fresh sequential voucher number, link the inbox item to it, and attach the OCR document to the verifikat'
: 'create a posted journal entry with a fresh sequential voucher number',
: inboxItemId
? 'create a posted journal entry with a fresh sequential voucher number and link the inbox item to it (the item has no stored document, so nothing is attached)'
: 'create a posted journal entry with a fresh sequential voucher number',
},
actor,
{
description: 'After commit, confirm the new verifikation lands on the right accounts with gnubok_get_general_ledger or gnubok_query_journal.',
tool: 'gnubok_query_journal',
},
{ dateForPeriodCheck: entryDate },
{
dateForPeriodCheck: entryDate,
// Advisory only (the approver decides): a verifikat for a received
// handling posted without its document fails BFL 5 kap 6§, and the
// repair after posting is storno + a consumed voucher number. The
// inbox-item variant avoids a dead-end "restage with inbox_item_id"
// instruction when inbox_item_id WAS supplied but the item carries
// no stored document. IB entries are exempt: a migrated ingående
// balans has no kvitto to attach and warning on it would be a false
// positive that trains approvers to ignore the warning.
...(inboxDocumentId || isOpeningBalance
? {}
: {
complianceNote: inboxItemId
? 'No underlag attached (document_attached: false): the inbox item has no stored document, so nothing will be attached. Upload the handling via gnubok_upload_document and restage, or link it to the posted verifikat with gnubok_link_document_to_voucher.'
: 'No underlag attached (document_attached: false). For a received handling, BFL 5 kap 6§ requires the document itself to serve as the verifikation: restage with inbox_item_id (upload via gnubok_upload_document first if needed), or link the document to the posted verifikat with gnubok_link_document_to_voucher. A filename in description or notes is not underlag.',
}),
},
)
},
},
@@ -16586,7 +16629,7 @@ export const tools: McpTool[] = [
{
name: 'gnubok_list_pending_operations',
title: 'List Pending Operations',
description: 'List staged pending_operations. Filter by status (default pending), risk_level, or operation_type. Approve via gnubok_approve_pending_operation, discard via gnubok_reject_pending_operation. render_ui=true opens the approval widget.',
description: 'List staged pending_operations. Approve via gnubok_approve_pending_operation, reject via gnubok_reject_pending_operation; without pending_operations:approve use /pending. render_ui=true opens the approval widget.',
inputSchema: {
type: 'object',
additionalProperties: false,
@@ -16652,7 +16695,7 @@ export const tools: McpTool[] = [
{
name: 'gnubok_approve_pending_operation',
title: 'Approve Pending Operation',
description: "Commit a staged pending_operation the user has explicitly authorised. risk_level=high requires confirmed=true: surface the BFL 5 kap 5§ irreversibility first. The /pending web UI offers an equivalent commit path.",
description: "Commit a staged pending_operation the user has explicitly authorised. risk_level=high requires confirmed=true: surface the BFL 5 kap 5§ irreversibility and any preview compliance_warning first. The /pending web UI is an equivalent commit path.",
inputSchema: {
type: 'object',
additionalProperties: false,