From a894af11d05dc96a584f02a91c6a0760c9caf160 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Sat, 6 Jun 2026 11:47:21 +0200 Subject: [PATCH] feat(mcp): tool titles + Connect-with-Claude docs for Connectors Directory readiness (P0-4) (#682) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mcp): tool titles + Connect-with-Claude docs for Connectors Directory readiness Add a top-level Tool.title (MCP spec 2025-06-18) to all 92 MCP tools and flow it through the tools/list serializer for the Claude Connectors Directory. Trim the ~10 longest tool descriptions toward 180-200 chars (semantics + agent hints preserved) to partly offset the added bytes, and raise the payload-size bench ceiling 32K → 36K with headroom reserved for upcoming Skatteverket tools. Ship a "Connect with Claude" docs page (OAuth 2.1 connector + npx gnubok-mcp stdio bridge, sample sandbox prompts, 10-minute reviewer test, support pointer), wire it into the docs nav, and add a README to the packages/gnubok-mcp npm bridge. Bump one categorized sandbox business expense above 4 000 kr so the VAT close-check demo surfaces a high-value receipt-less expense. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(mcp): restore six spaces lost in the description-trimming pass Review finding (Greptile P2): the trim deleted single spaces in six tool descriptions ("Stages foruser", "länkatill", "kundfordran(1510)", …), producing malformed text in tools/list. All six were pure space deletions with no token gain — restored verbatim. Swept all 92 descriptions for residual merge artifacts; the two remaining matches are pre-existing function-call notation from main. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(compliance): correct BFL citation in seed + mark 4000 kr as app heuristic Swedish compliance review findings: - The seed comment cited BFL 5 kap 6§ as requiring verifikat on high-value affärshändelser; 6§ governs verifikationens innehåll. Rephrased: BFL 5 kap 6-7§ require every affärshändelse to be documented — the 4 000 kr cut-off is the tool's own heuristic, not a statutory threshold. - The connect-claude docs read as though 4 000 kr were a legal floor (conflatable with the förenklad faktura ceiling, ML 17 kap 26-28§). Clarified that BFL requires underlag regardless of amount. Dismissed as by-design: the gnubok_sk_test_... README placeholders (standard practice; the prefix schema is already public in llms.txt). Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- app/api/sandbox/seed/route.ts | 7 +- app/docs/api/connect-claude.md/route.ts | 13 ++ app/docs/api/connect-claude/page.tsx | 18 +++ extensions/general/mcp-server/README.md | 2 +- .../__tests__/payload-size.bench.test.ts | 9 +- extensions/general/mcp-server/server.ts | 117 ++++++++++++++++-- lib/docs/content/connect-claude.ts | 78 ++++++++++++ lib/docs/nav.ts | 1 + packages/gnubok-mcp/README.md | 56 +++++++++ packages/gnubok-mcp/package.json | 3 +- 10 files changed, 289 insertions(+), 15 deletions(-) create mode 100644 app/docs/api/connect-claude.md/route.ts create mode 100644 app/docs/api/connect-claude/page.tsx create mode 100644 lib/docs/content/connect-claude.ts create mode 100644 packages/gnubok-mcp/README.md diff --git a/app/api/sandbox/seed/route.ts b/app/api/sandbox/seed/route.ts index db2c9017..ff1ceb34 100644 --- a/app/api/sandbox/seed/route.ts +++ b/app/api/sandbox/seed/route.ts @@ -504,7 +504,12 @@ export async function POST(request: Request) { company_id: companyId, date: toDateStr(fiveDaysAgo), description: 'SJ BILJETT', - amount: -2500, + // > 4 000 kr categorized business expense with no attached underlag, + // so gnubok_vat_close_check surfaces a non-empty blocker list. + // (BFL 5 kap 6–7§ require every affärshändelse to be documented with + // underlag; the 4 000 kr cut-off is the tool's own high-value + // heuristic, not a statutory threshold.) + amount: -4500, category: 'expense_travel', is_business: true, merchant_name: 'SJ', diff --git a/app/docs/api/connect-claude.md/route.ts b/app/docs/api/connect-claude.md/route.ts new file mode 100644 index 00000000..cd8a3480 --- /dev/null +++ b/app/docs/api/connect-claude.md/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from 'next/server' +import { CONNECT_CLAUDE_MD } from '@/lib/docs/content/connect-claude' +import { withPublicSecurityHeaders } from '@/lib/api/v1/security-headers' + +export async function GET() { + return new NextResponse(CONNECT_CLAUDE_MD, { + status: 200, + headers: withPublicSecurityHeaders({ + 'Content-Type': 'text/markdown; charset=utf-8', + 'Cache-Control': 'public, max-age=300, s-maxage=300', + }), + }) +} diff --git a/app/docs/api/connect-claude/page.tsx b/app/docs/api/connect-claude/page.tsx new file mode 100644 index 00000000..1959b8e9 --- /dev/null +++ b/app/docs/api/connect-claude/page.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from 'next' +import { DocsLayout } from '@/components/docs/DocsLayout' +import { DocsMarkdown } from '@/lib/docs/markdown' +import { CONNECT_CLAUDE_MD } from '@/lib/docs/content/connect-claude' + +export const metadata: Metadata = { + title: 'Connect with Claude · accounted API', + description: + 'Connect Accounted to Claude (claude.ai, Claude Desktop, Claude Code) via the MCP server — OAuth 2.1 connector or the npx gnubok-mcp stdio bridge.', +} + +export default function DocsApiConnectClaudePage() { + return ( + + + + ) +} diff --git a/extensions/general/mcp-server/README.md b/extensions/general/mcp-server/README.md index d0d21688..f53141e1 100644 --- a/extensions/general/mcp-server/README.md +++ b/extensions/general/mcp-server/README.md @@ -21,7 +21,7 @@ For internal Anthropic API usage (today only `extensions/general/invoice-inbox/l ## Payload-size watchdog -`payload-size.bench.test.ts` enforces a `tools/list` JSON payload ceiling (currently 25,000 tokens). If the test fires, the right answer is rarely "raise the ceiling" — instead, trim descriptions or leverage `gnubok_search_tools` (already deployed; tool definitions can defer to it for discovery rather than enumerating in `tools/list`). +`payload-size.bench.test.ts` enforces a `tools/list` JSON payload ceiling (currently 36,000 tokens — bumped from 32,000 when top-level `Tool.title` landed on all tools for Claude Connectors Directory readiness). If the test fires, the right answer is rarely "raise the ceiling" — instead, trim descriptions or leverage `gnubok_search_tools` (already deployed; tool definitions can defer to it for discovery rather than enumerating in `tools/list`). ## Where things live diff --git a/extensions/general/mcp-server/__tests__/payload-size.bench.test.ts b/extensions/general/mcp-server/__tests__/payload-size.bench.test.ts index b0b86ba9..88502e55 100644 --- a/extensions/general/mcp-server/__tests__/payload-size.bench.test.ts +++ b/extensions/general/mcp-server/__tests__/payload-size.bench.test.ts @@ -5,6 +5,7 @@ describe('tools/list payload size guard', () => { it('keeps the projected tools/list payload under the context-budget ceiling', () => { const projection = tools.map((t) => ({ name: t.name, + ...(t.title ? { title: t.title } : {}), description: t.description, inputSchema: t.inputSchema, ...(t.outputSchema ? { outputSchema: t.outputSchema } : {}), @@ -13,7 +14,7 @@ describe('tools/list payload size guard', () => { })) const payload = JSON.stringify({ tools: projection }) const approxTokens = Math.round(payload.length / 4) - // Ceiling progression: 20K → 25K → 30K → 31K → 31.5K. + // Ceiling progression: 20K → 25K → 30K → 31K → 31.5K → 32K → 36K. // * 20K → 25K when item 8 of the agent-native API plan landed // (additionalProperties: false on all inputSchemas + period_status in the // staged operation envelope). @@ -38,9 +39,13 @@ describe('tools/list payload size guard', () => { // faktura paid against an already-posted verifikat (no new bokföring), // which is exactly the fix for invoices imported from Fortnox as open // payables while their payment already exists in the SIE-imported GL. + // * 32K → 36K when top-level Tool.title (MCP spec 2025-06-18) landed on all + // 92 tools for Connectors Directory readiness; the ~10 longest descriptions + // were trimmed toward 180–200 chars to partly offset. Headroom reserved for + // the upcoming Skatteverket tools. // Long-term answer to growth is leaning harder on gnubok_search_tools — if this // fires again, prefer trimming descriptions or making a tool opt-in via search // before bumping further. - expect(approxTokens).toBeLessThan(32_000) + expect(approxTokens).toBeLessThan(36_000) }) }) diff --git a/extensions/general/mcp-server/server.ts b/extensions/general/mcp-server/server.ts index 2bb2dd99..94cf1db6 100644 --- a/extensions/general/mcp-server/server.ts +++ b/extensions/general/mcp-server/server.ts @@ -121,6 +121,10 @@ interface McpToolAnnotations { interface McpTool { name: string + // Top-level Tool.title per MCP spec 2025-06-18 (human-facing label for + // directory listings; distinct from annotations.title). Short Title Case + // noun phrase. Flows out via the tools/list serializer below. + title?: string description: string inputSchema: Record outputSchema?: Record @@ -1399,6 +1403,7 @@ function previousPeriodArgs( export const tools: McpTool[] = [ { name: 'gnubok_search_tools', + title: 'Search MCP Tools', description: 'Search Accounted MCP tools by keyword and return their schemas at a chosen detail level. Call this first when looking for a capability — avoids loading every tool schema upfront.', inputSchema: { type: 'object', @@ -1517,6 +1522,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_skills', + title: 'List Domain Skills', description: 'List available domain-knowledge skills filtered to this company (entity type, VAT, payroll). Workflow guides + loaded specialty atoms. Pass include_all=true to see hidden skills. Call gnubok_load_skill(slug) for any body.', inputSchema: { type: 'object', @@ -1641,6 +1647,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_load_skill', + title: 'Load Domain Skill', description: 'Load a skill body by slug. Workflow slugs are flat (e.g. "month-end-close"); atom slugs match registry ids (e.g. "vertical/konsult-it", "modifier/holding-ab"). Call gnubok_list_skills to find slugs.', inputSchema: { type: 'object', @@ -1704,7 +1711,8 @@ export const tools: McpTool[] = [ { name: 'gnubok_remember_fact', - description: 'Capture a durable fact, preference, pattern, or correction about the company. Use mid-conversation when the user says something the agent should remember next time. Writes immediately — does not stage. Use sparingly for foundational signal, not for one-off context.', + title: 'Remember Company Fact', + description: 'Capture a durable fact, preference, or correction about the company. Use mid-conversation when the user says something to remember next time. Writes immediately — does not stage. Use sparingly.', inputSchema: { type: 'object', additionalProperties: false, @@ -1818,6 +1826,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_forget_fact', + title: 'Forget Company Fact', description: 'Deactivate a memory entry by id. Use when the user explicitly asks to forget something or supersedes it. The row is kept for audit (is_active=false) but no longer surfaces in prompts.', inputSchema: { type: 'object', @@ -1860,6 +1869,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_feedback', + title: 'Send Agent Feedback', description: 'Report agent-side feedback: missing tool, wrong description, skill gap, or a positive signal. Goes to event_log for product-team triage. Rate-limited 1/min/key.', inputSchema: { type: 'object', @@ -1958,6 +1968,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_agent_briefing', + title: 'Get Agent Briefing', description: 'Bootstrap this company\'s specialized accountant context in one call: profile_summary, the atoms loaded for the company (metadata only — call gnubok_load_skill for bodies), and the top-30 active memories. Call once at session start.', inputSchema: { type: 'object', @@ -2088,7 +2099,8 @@ export const tools: McpTool[] = [ { name: 'gnubok_create_transactions', - description: 'Stage one or more transactions for the user to approve. Each item creates a separate pending operation; commit each via gnubok_approve_pending_operation when the user authorises. Useful for ingesting rows from external sources (Airtable, CSVs, etc.). Max 10 per call.', + 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.', outputSchema: { type: 'object', additionalProperties: false, @@ -2200,6 +2212,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_uncategorized_transactions', + title: 'List Uncategorized Transactions', description: 'List bank transactions with no journal entry yet, newest first. Paginated.', inputSchema: { type: 'object', @@ -2270,6 +2283,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_transactions_without_documents', + title: 'List Transactions Missing Receipts', description: 'List BANK TRANSACTIONS booked without an attached underlag. For imported/manual verifikat (no bank tx row) call gnubok_list_verifikat_without_documents — this tool only covers bank-driven entries.', inputSchema: { type: 'object', @@ -2349,6 +2363,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_verifikat_without_documents', + title: 'List Verifikat Missing Documents', description: 'List POSTED journal entries (verifikat) that have no document_attachments row. Covers SIE-imported, manual and salary vouchers that the transactions-based tool misses. Newest first, paginated.', inputSchema: { type: 'object', @@ -2446,7 +2461,8 @@ export const tools: McpTool[] = [ { name: 'gnubok_categorize_transaction', - description: 'Categorize a bank transaction. Stages the journal entry; commit via gnubok_approve_pending_operation when the user authorises. If the row has an attached underlag, the tool reads its extracted_data and rejects vat_treatment="reverse_charge" when the seller already charged VAT.', + title: 'Categorize Bank Transaction', + description: 'Categorize a bank transaction. Stages the journal entry; commit via gnubok_approve_pending_operation. If an underlag is attached it rejects vat_treatment="reverse_charge" when the seller already charged VAT.', inputSchema: { type: 'object', additionalProperties: false, @@ -2529,6 +2545,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_receipt_matcher', + title: 'Receipt Matcher Widget', description: 'Open an interactive widget for drag-and-drop receipt-to-transaction matching. Renders inline in compatible clients.', inputSchema: { type: 'object', @@ -2581,6 +2598,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_customers', + title: 'List Customers', description: 'List all customers for the active company. Use to look up customer_id for invoice creation.', inputSchema: { type: 'object', additionalProperties: false, properties: {} }, outputSchema: { @@ -2613,6 +2631,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_create_customer', + title: 'Create Customer', description: 'Stage a new customer. Stages for user approval — NOT created until approved in the web app. EU VAT numbers trigger VIES validation.', outputSchema: STAGED_OPERATION_SCHEMA, inputSchema: { @@ -2693,6 +2712,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_invoices', + title: 'List Customer Invoices', description: 'List invoices for the active company, newest first. Optional status filter.', inputSchema: { type: 'object', @@ -2754,6 +2774,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_create_invoice', + title: 'Create Customer Invoice', description: 'Stage a new invoice. Validates inputs, calculates VAT preview. Stages for user approval — invoice number assigned at approval.', outputSchema: STAGED_OPERATION_SCHEMA, inputSchema: { @@ -2898,6 +2919,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_trial_balance', + title: 'Trial Balance (Råbalans)', description: 'Trial balance (huvudbok) for a fiscal period — all account balances with debit/credit totals. Defaults to most recent period.', inputSchema: { type: 'object', @@ -3016,6 +3038,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_vat_report', + title: 'VAT Declaration (Momsdeklaration)', description: 'VAT declaration (momsdeklaration, SKV 4700) for a period. Returns all rutor; ruta49 = VAT to pay (positive) or refund (negative). Pass render_ui=true to also open the review widget (claude.ai / Desktop).', outputSchema: VAT_REPORT_OUTPUT_SCHEMA, inputSchema: { @@ -3053,6 +3076,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_vat_review_widget', + title: 'VAT Review Widget', description: 'Open the interactive VAT review widget for a period. Equivalent to gnubok_get_vat_report(render_ui=true); kept as an alias for clients pinned to this tool name.', inputSchema: { type: 'object', @@ -3079,7 +3103,8 @@ export const tools: McpTool[] = [ { name: 'gnubok_vat_close_check', - description: "Answer 'can I close VAT?' in one call. Returns SKV 4700 rutor + blocker scan (uncategorized, unapproved supplier invoices, reconciliation diff, missing receipts ≥ 4000 kr, reverse-charge mirroring) + period sanity ratios + Skatteverket deadline + ready_to_close.", + title: 'VAT Close Check (Momsdeklaration)', + description: "Answer 'can I close VAT?' in one call. Returns SKV 4700 rutor + blocker scan (uncategorized, unapproved supplier invoices, reconciliation diff, missing receipts ≥ 4000 kr, reverse-charge) + ready_to_close.", inputSchema: { type: 'object', additionalProperties: false, @@ -3129,6 +3154,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_kpi_report', + title: 'Business KPI Report', description: 'Business KPIs for a fiscal period: gross margin, net result, cash position, receivables, expense ratio, payment days, VAT liability, monthly trend.', inputSchema: { type: 'object', @@ -3231,6 +3257,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_income_statement', + title: 'Income Statement (Resultaträkning)', description: 'Income statement (resultaträkning) for a fiscal period: revenue, expenses, net result by account category.', inputSchema: { type: 'object', @@ -3287,6 +3314,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_mark_invoice_as_paid', + title: 'Mark Invoice as Paid', description: 'Mark an invoice as paid and create the payment journal entry. Stages for approval. Status must be sent or overdue.', inputSchema: { type: 'object', @@ -3344,6 +3372,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_send_invoice', + title: 'Send Invoice by Email', description: 'Send invoice via email with PDF attachment. Stages for approval. Requires customer email + email service configured.', inputSchema: { type: 'object', @@ -3403,6 +3432,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_mark_invoice_as_sent', + title: 'Mark Invoice as Sent', description: 'Mark a draft invoice as sent without sending email (when delivered manually). Stages for approval. Status must be draft.', inputSchema: { type: 'object', @@ -3456,6 +3486,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_suppliers', + title: 'List Suppliers (Leverantörer)', description: 'List all suppliers (leverantörer) with contact and payment details, sorted by name.', inputSchema: { type: 'object', additionalProperties: false, properties: {} }, outputSchema: { @@ -3488,6 +3519,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_create_supplier', + title: 'Create Supplier (Leverantör)', description: 'Stage a new supplier (leverantör). Stages for user approval — NOT created until approved in the web app. Use to add a vendor before booking a supplier invoice or matching expenses.', outputSchema: STAGED_OPERATION_SCHEMA, inputSchema: { @@ -3623,6 +3655,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_supplier_invoices', + title: 'List Supplier Invoices', description: 'List supplier invoices (leverantörsfakturor), sorted by due date. Optional status filter; "to_pay" combines approved+overdue.', inputSchema: { type: 'object', @@ -3680,6 +3713,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_counterparty_templates', + title: 'List Counterparty Templates', description: 'List active counterparty categorization templates — learned patterns from prior categorizations used for auto-matching new transactions.', inputSchema: { type: 'object', @@ -3728,6 +3762,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_suggest_categories', + title: 'Suggest Transaction Categories', description: 'Suggest categories for uncategorized transactions using mapping rules, pattern matching, history, and counterparty templates. Up to 20 transactions per call.', inputSchema: { type: 'object', @@ -3830,6 +3865,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_accounts', + title: 'List Chart of Accounts (Kontoplan)', description: 'List chart of accounts (kontoplan). account_class: 1=assets, 2=liabilities, 3=revenue, 4–7=expenses, 8=financial.', inputSchema: { type: 'object', @@ -3879,6 +3915,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_balance_sheet', + title: 'Balance Sheet (Balansräkning)', description: 'Balance sheet (balansräkning) for a fiscal period: assets, equity, and liabilities sections with totals + balance check.', inputSchema: { type: 'object', @@ -3931,6 +3968,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_general_ledger', + title: 'General Ledger (Huvudbok)', description: 'General ledger (huvudbok) for a fiscal period: per-account opening balance, entries, closing balance. Optional account range filter. For ad-hoc cross-account, amount, or free-text line queries use gnubok_query_journal.', inputSchema: { type: 'object', @@ -3973,6 +4011,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_query_journal', + title: 'Query Journal Lines', description: "Flexible journal-line query — replaces chained ledger calls for ad-hoc questions. Filters: accounts, date range, amount range, voucher series/number, source type, status, project, cost center, free-text. Returns lines with parent voucher metadata + totals.", inputSchema: { type: 'object', @@ -4297,6 +4336,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_ar_ledger', + title: 'AR Ledger (Kundreskontra)', description: 'Accounts receivable ledger (kundreskontra): outstanding customer invoices with aging.', inputSchema: { type: 'object', @@ -4320,6 +4360,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_supplier_ledger', + title: 'AP Ledger (Leverantörsreskontra)', description: 'Accounts payable ledger (leverantörsreskontra): outstanding supplier invoices with aging.', inputSchema: { type: 'object', @@ -4345,6 +4386,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_match_transaction_to_invoice', + title: 'Match Transaction to Invoice', description: 'Match a bank transaction (income, amount>0) to a customer invoice. Confirm tx date/amount and invoice number/customer match before staging — preview mirrors what you pass. Supports partial payments and auto-storno of prior categorization.', inputSchema: { type: 'object', @@ -4421,7 +4463,8 @@ export const tools: McpTool[] = [ { name: 'gnubok_match_batch_allocate', - description: 'Allocate 1 bank tx across N customer OR N supplier invoices (samlingsbetalning, BFL 5 kap 6§). Use when one receipt covers many invoices, or one transfer pays many bills. Customer kind requires income tx; supplier kind requires expense. Never mix kinds. Stages.', + title: 'Batch-Allocate Payment', + description: 'Allocate 1 bank tx across N customer OR N supplier invoices (samlingsbetalning, BFL 5 kap 6§). Use when one receipt covers many invoices or one transfer pays many bills. Customer needs income, supplier expense. Stages.', inputSchema: { type: 'object', additionalProperties: false, @@ -4609,6 +4652,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_link_transaction_to_journal_entry', + title: 'Link Transaction to Verifikat', description: 'Link 1 bank tx to an already-posted verifikat (no new bokföring). Use when the user booked the affärshändelse manually. Pass invoice_id to also settle a kundfaktura. Stages.', inputSchema: { type: 'object', @@ -4762,7 +4806,8 @@ export const tools: McpTool[] = [ { name: 'gnubok_bulk_book_transactions', - description: 'Bulk-book N bank txs on the same date into 1 samlingsverifikat (BFL 5 kap 6§). Two paths: link N txs to an existing posted verifikat, or create a new verifikat from caller-supplied lines. All txs must share date + direction. Docs on the txs inherit. Stages.', + 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. All txs share date + direction. Stages.', inputSchema: { type: 'object', additionalProperties: false, @@ -4948,6 +4993,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_find_voucher_candidates_for_invoice', + title: 'Find Voucher Candidates (Invoice)', description: 'List posted verifikat that credit kundfordran (1510) and could be the payment for this invoice. Use before gnubok_link_invoice_to_voucher when the user wants to mark a faktura paid against an existing verifikation (no new bokföring).', inputSchema: { type: 'object', @@ -5013,6 +5059,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_link_invoice_to_voucher', + title: 'Link Invoice to Voucher', description: 'Markera en faktura som betald genom att länka till en befintlig verifikation som redan krediterar kundfordran (1510). Ingen ny verifikation skapas. Hitta kandidater med gnubok_find_voucher_candidates_for_invoice först.', inputSchema: { type: 'object', @@ -5099,6 +5146,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_find_voucher_candidates_for_supplier_invoice', + title: 'Find Voucher Candidates (Supplier Invoice)', description: 'List posted verifikat that debit leverantörsskuld (2440) and could be the payment for this supplier invoice. Use before gnubok_link_supplier_invoice_to_voucher when marking a leverantörsfaktura paid against an existing verifikation (no new bokföring).', inputSchema: { type: 'object', @@ -5164,6 +5212,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_link_supplier_invoice_to_voucher', + title: 'Link Supplier Invoice to Voucher', description: 'Markera en leverantörsfaktura som betald genom att länka till en befintlig verifikation som redan debiterar leverantörsskuld (2440). Ingen ny verifikation skapas. Hitta kandidater med gnubok_find_voucher_candidates_for_supplier_invoice först.', inputSchema: { type: 'object', @@ -5250,7 +5299,8 @@ export const tools: McpTool[] = [ { name: 'gnubok_auto_match_period', - description: "Bulk reconciliation: scan unmatched income transactions in a date range and propose invoice matches with confidence + reasoning. dry_run=true (default) previews without staging; dry_run=false stages every match above confidence_threshold as a pending operation.", + title: 'Auto-Match Period Income', + description: "Bulk reconciliation: scan unmatched income in a date range and propose invoice matches with confidence + reasoning. dry_run=true (default) previews; dry_run=false stages matches above confidence_threshold.", inputSchema: { type: 'object', additionalProperties: false, @@ -5440,6 +5490,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_fiscal_periods', + title: 'List Fiscal Periods', description: 'List all fiscal periods (räkenskapsperioder) with status: active (open), locked (no new entries), or closed (year-end completed).', inputSchema: { type: 'object', additionalProperties: false, properties: {} }, outputSchema: { @@ -5483,6 +5534,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_reconciliation_status', + title: 'Bank Reconciliation Status', description: 'Bank reconciliation status: matched/unmatched counts, match rate, bank vs ledger balance, difference. Optional date range.', inputSchema: { type: 'object', @@ -5510,6 +5562,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_upload_document', + title: 'Upload Document to Inbox', description: 'Upload a PDF/JPEG/PNG/HEIC/WebP (max 20 MB) to the inbox. Runs deterministic field extraction on text-based PDFs.', inputSchema: { type: 'object', @@ -5622,6 +5675,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_inbox_items', + title: 'List Inbox Items', description: 'List document inbox items. Each has a `processed` flag covering all terminal links (transaction match, supplier invoice, or journal entry), so a booked receipt counts as done. unprocessed_only=true returns only docs still needing handling.', inputSchema: { type: 'object', @@ -5720,6 +5774,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_inbox_item', + title: 'Get Inbox Item', description: 'Get a single inbox item with complete extracted data, supplier match, email metadata, and timestamps.', inputSchema: { type: 'object', @@ -5755,6 +5810,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_create_supplier_invoice_from_inbox', + title: 'Create Supplier Invoice from Inbox', description: "Atomic: turn an OCR'd inbox item into a staged supplier invoice. Resolves supplier (matched or via org_number/name), assembles line items from extracted_data, applies VAT + FX, attaches the source document. Stages for human review; honors dry_run.", inputSchema: { type: 'object', @@ -5946,6 +6002,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_unmatched_documents', + title: 'List Unmatched Documents', description: 'List inbox documents not yet attached to any bank transaction or supplier invoice. Returns vendor/amount/currency/date hints. The amount is in the invoice currency — FX-normalise before comparing to transactions.amount.', inputSchema: { type: 'object', @@ -6100,6 +6157,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_get_document_content', + title: 'Get Document Content', description: 'Get a 5-minute signed download URL for a document so the agent can read its contents (e.g. with vision). Use after gnubok_list_unmatched_documents to inspect a specific PDF before deciding which transaction it matches.', inputSchema: { type: 'object', @@ -6166,6 +6224,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_attach_document_to_transaction', + title: 'Attach Document to Transaction', description: 'Stage attaching a document to a bank transaction. Verify tx (date, amount, counterparty) and document (filename, vendor, amount) match first — the preview shown to the human reviewer mirrors what you pass here. Stages for approval.', inputSchema: { type: 'object', @@ -6295,6 +6354,7 @@ export const tools: McpTool[] = [ // ── Payroll (Lönehantering) ────────────────────────────────── { name: 'gnubok_list_employees', + title: 'List Employees', description: 'List employees for the active company. Personnummer returned masked (YYYYMMDD-XXXX).', inputSchema: { type: 'object', @@ -6328,6 +6388,7 @@ export const tools: McpTool[] = [ }, { name: 'gnubok_get_salary_run', + title: 'Get Salary Run', description: 'Get salary run with status, totals, per-employee breakdown (gross, tax, net, avgifter, vacation accrual) and step-by-step calculation breakdown.', inputSchema: { type: 'object', @@ -6357,6 +6418,7 @@ export const tools: McpTool[] = [ }, { name: 'gnubok_get_salary_journal', + title: 'Salary Journal (Lönejournal)', description: 'Salary journal (lönejournal) for a year: per-employee per-month rows + yearly totals.', inputSchema: { type: 'object', @@ -6375,6 +6437,7 @@ export const tools: McpTool[] = [ }, { name: 'gnubok_create_salary_run', + title: 'Create Salary Run', description: 'Stage creation of a draft salary run for a period + base lines for all active employees. Commit via gnubok_approve_pending_operation; then run gnubok_calculate_salary_run. Final booking happens in the web UI.', inputSchema: { type: 'object', @@ -6430,6 +6493,7 @@ export const tools: McpTool[] = [ }, { name: 'gnubok_calculate_salary_run', + title: 'Calculate Salary Run', description: 'Calculate a draft salary run: tax, avgifter, vacation accrual, totals. Run must be in draft status.', inputSchema: { type: 'object', @@ -6475,6 +6539,7 @@ export const tools: McpTool[] = [ }, { name: 'gnubok_generate_agi', + title: 'Generate AGI Declaration', description: 'Stage AGI XML generation (Arbetsgivardeklaration) for a salary run. High-risk: produces statutory Skatteverket underlag (BFL 7-year retention). Commit via gnubok_approve_pending_operation.', inputSchema: { type: 'object', @@ -6523,6 +6588,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_close_period', + title: 'Close Fiscal Period', description: 'Stage period close (irreversible per BFL). Requires period locked + year-end closing entry posted. High-risk — always staged, never auto-committed.', inputSchema: { type: 'object', @@ -6578,6 +6644,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_lock_period', + title: 'Lock Fiscal Period', description: 'Stage period lock — blocks new entries. Requires zero unbooked business transactions. High-risk, always staged.', inputSchema: { type: 'object', @@ -6645,6 +6712,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_uncategorize_transaction', + title: 'Uncategorize Transaction', description: 'Stage uncategorize: reverses linked journal entry via storno (never deletes) and clears the category. Stages for approval.', inputSchema: { type: 'object', @@ -6710,6 +6778,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_export_sie', + title: 'Export SIE File', description: 'Generate SIE-4 file for a fiscal period (standard Swedish bookkeeping interchange format). Returns SIE text content.', inputSchema: { type: 'object', @@ -6767,7 +6836,8 @@ export const tools: McpTool[] = [ { name: 'gnubok_audit_package', - description: "Single-call audit package for a fiscal period: SIE-4 + reports (trial balance, income statement, balance sheet, general ledger, journal register, VAT) + receipts + audit log + voucher gaps, zipped. Returns a 1-hour signed download URL. Long-running on large datasets.", + title: 'Generate Audit Package', + description: "Single-call audit package for a fiscal period: SIE-4 + reports (trial balance, income statement, balance sheet, general ledger, journal, VAT) + receipts + audit log + voucher gaps, zipped. 1-hour signed URL.", inputSchema: { type: 'object', additionalProperties: false, @@ -6907,7 +6977,8 @@ export const tools: McpTool[] = [ { name: 'gnubok_year_end_readiness', - description: "Pre-flight before irreversible gnubok_run_year_end. Returns ready (bool) + ordered blockers (drafts, voucher gaps, sequence mismatches, unbalanced trial balance, FX revaluation needed) + warnings + optional preview of the closing entry. Use this before staging year-end.", + title: 'Year-End Readiness Check', + description: "Pre-flight before irreversible gnubok_run_year_end. Returns ready (bool) + ordered blockers (drafts, voucher gaps, sequence mismatches, unbalanced TB, FX revaluation) + optional closing-entry preview.", inputSchema: { type: 'object', additionalProperties: false, @@ -7017,6 +7088,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_run_year_end', + title: 'Run Year-End Closing (Bokslut)', description: 'Stage year-end closing: zero result accounts (class 3–8) into 2099, lock period, create next period, seed opening balances. High-risk, always staged.', inputSchema: { type: 'object', @@ -7061,6 +7133,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_set_opening_balances', + title: 'Set Opening Balances (Ingående Balans)', description: 'Stage opening-balance entry: copy class 1–2 closing balances from a closed period into the next period.', inputSchema: { type: 'object', @@ -7110,6 +7183,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_run_currency_revaluation', + title: 'Run Currency Revaluation', description: 'Stage currency revaluation: revalue open FX receivables/payables to closing-date rate (posts 3960/7960). One per period max.', inputSchema: { type: 'object', @@ -7143,6 +7217,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_list_voucher_gaps', + title: 'List Voucher Gaps', description: 'List voucher number gaps in a fiscal period (BFNAR 2013:2 audit requirement). Each gap shows whether it has an explanation.', inputSchema: { type: 'object', @@ -7215,6 +7290,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_explain_voucher_gap', + title: 'Explain Voucher Gap', description: 'Stage explanation for a voucher gap (BFNAR 2013:2 compliance — every gap needs a documented reason).', inputSchema: { type: 'object', @@ -7261,6 +7337,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_approve_supplier_invoice', + title: 'Approve Supplier Invoice', description: 'Stage approval of a registered supplier invoice (registered → approved). High-risk, always staged.', inputSchema: { type: 'object', @@ -7303,6 +7380,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_credit_supplier_invoice', + title: 'Credit Supplier Invoice (Kreditfaktura)', description: 'Stage credit-note (kreditfaktura) for a supplier invoice: mirror invoice with negative effect + reverses registration JE (accrual).', inputSchema: { type: 'object', @@ -7344,6 +7422,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_convert_invoice', + title: 'Convert Proforma to Invoice', description: 'Stage conversion of a proforma invoice to a real invoice. Allocates F-series number, copies items, marks proforma cancelled.', inputSchema: { type: 'object', @@ -7386,6 +7465,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_unlock_period', + title: 'Unlock Fiscal Period', description: 'Stage period unlock — clears locked_at so entries can be posted again. Cannot unlock a closed period. High-risk, always staged.', inputSchema: { type: 'object', @@ -7434,6 +7514,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_credit_invoice', + title: 'Credit Customer Invoice (Kreditfaktura)', description: 'Stage credit note (kreditfaktura) for a customer invoice: KR- prefixed mirror invoice + reverses original JE (accrual). Original must be sent/paid/overdue and not already credited.', inputSchema: { type: 'object', @@ -7487,6 +7568,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_import_sie', + title: 'Import SIE File', description: 'Stage SIE-file import (types 1–4, CP437/UTF-8/Latin-1). On commit creates fiscal period, opening balances, and journal entries. High-risk, always staged.', inputSchema: { type: 'object', @@ -7616,6 +7698,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_undo_sie_import', + title: 'Undo SIE Import', description: 'Stage undo of a completed SIE import: hard-deletes its entries (transactions + opening balance), detaches docs, resets voucher_sequences, marks the row \'undone\' so the file can be re-imported. Use after a botched import. Period must be open. HIGH risk.', inputSchema: { type: 'object', @@ -7716,6 +7799,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_create_voucher', + title: 'Create Manual Voucher (Verifikation)', description: 'Stage a manual verifikation with arbitrary balanced lines. Use for capitalization (1010), period-end accruals, FX adjustments, rättelseposter outside categorize_transaction. Pass inbox_item_id to book a kvitto direct — links inbox + attaches doc. HIGH risk.', inputSchema: { type: 'object', @@ -7951,7 +8035,8 @@ export const tools: McpTool[] = [ { name: 'gnubok_correct_entry', - description: 'Stage a rättelse for a posted verifikation per BFL 5 kap 5§ — storno + new corrected entry in the original period (never in-place edit). Use for partial fixes like 2641 → 2614/2645 while preserving the expense leg. Account drives momsdeklaration ruta, not tax_code. HIGH risk.', + title: 'Correct Posted Entry (Rättelse)', + description: 'Stage a rättelse for a posted verifikation per BFL 5 kap 5§ — storno + corrected entry in the original period (never in-place edit). Use for partial fixes like 2641 → 2614/2645. Account drives ruta. HIGH risk.', inputSchema: { type: 'object', additionalProperties: false, @@ -8112,6 +8197,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_reverse_journal_entry', + title: 'Reverse Journal Entry (Storno)', description: 'Stage a storno: inverts debits/credits; original stays visible per BFL 5 kap. Use only when the affärshändelse should never have been booked (duplicate, ghost, test). If booked wrong, use gnubok_correct_entry; for refunds, gnubok_credit_invoice. HIGH risk.', inputSchema: { type: 'object', @@ -8264,6 +8350,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_propose_dispositioner', + title: 'Propose Year-End Dispositioner', description: 'Read-only proposal of bokslutsdispositioner for a fiscal period: periodiseringsfond (avsättning + obligatorisk återföring), överavskrivningar, SLP, bolagsskatt. Call before staging postings.', inputSchema: { @@ -8289,6 +8376,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_propose_accruals', + title: 'Propose Accruals (Periodiseringar)', description: 'Read-only proposal of periodiseringar (förutbetalda/upplupna kostnader). Currently surfaces the vacation-liability change; manual prepaid/accrued entries are submitted by the UI form.', inputSchema: { @@ -8311,6 +8399,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_propose_annual_depreciation', + title: 'Propose Annual Depreciation (Avskrivning)', description: 'Read-only per-asset planenlig avskrivning proposal for a fiscal period. Reads the asset register and existing depreciation schedules. Call before staging the post.', inputSchema: { @@ -8333,6 +8422,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_post_annual_depreciation', + title: 'Post Annual Depreciation (Avskrivning)', description: 'Stage planenlig avskrivning posts — one journal entry per asset for independent reversibility. Mid-risk, always staged.', inputSchema: { @@ -8417,6 +8507,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_preview_arsredovisning', + title: 'Preview Annual Report (Årsredovisning)', description: 'Read-only K2 årsredovisning preview for a fiscal period. Returns flerårsöversikt, eget-kapital-förändring, RR, BR, K2 noter, signature slots. PDF download is via UI.', inputSchema: { @@ -8439,6 +8530,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_preview_ef_declaration', + title: 'Preview EF Declaration (NE-bilaga)', description: 'Read-only EF declaration preview: egenavgifter schablonavdrag, räntefördelning, periodiseringsfond, expansionsfond. All declaration-only, never booked. Pass kapitalunderlag and prior-year amounts as inputs.', inputSchema: { @@ -8482,6 +8574,7 @@ export const tools: McpTool[] = [ // Mirrors the /pending web UI for agents that self-review before committing. { name: 'gnubok_list_pending_operations', + title: 'List Pending Operations', description: 'List staged pending_operations. Filter by status (default pending), risk_level, or operation_type. Use to review the queue before calling gnubok_approve_pending_operation or gnubok_reject_pending_operation.', inputSchema: { type: 'object', @@ -8539,6 +8632,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_approve_pending_operation', + title: 'Approve Pending Operation', description: "Commit a staged pending_operation when the user has explicitly authorised the operation_id. risk_level=high requires confirmed=true — surface the BFL 5 kap 5§ irreversibility to the user first. The /pending web UI offers an equivalent commit path.", inputSchema: { type: 'object', @@ -8679,6 +8773,7 @@ export const tools: McpTool[] = [ { name: 'gnubok_reject_pending_operation', + title: 'Reject Pending Operation', description: 'Reject a staged pending_operation without executing it. Status flips to rejected; no journal entries, invoices, or other side-effects created. Idempotent on already-resolved ops (returns 409).', inputSchema: { type: 'object', @@ -8778,6 +8873,7 @@ export const tools: McpTool[] = [ // ── Bring-your-own-extraction for inbox items ──────────────── { name: 'gnubok_set_inbox_extracted_data', + title: 'Set Inbox Extracted Data', description: 'Replace extracted_data on an inbox item with agent-supplied fields (bring-your-own-extraction). Use when your own pipeline parses the document better than Accounted\'s OCR. Follow with gnubok_create_supplier_invoice_from_inbox to stage.', inputSchema: { type: 'object', @@ -9305,6 +9401,7 @@ export async function handleMcpRequest(request: Request): Promise { jsonRpc(id ?? null, { tools: allowedTools.map((t) => ({ name: t.name, + ...(t.title ? { title: t.title } : {}), description: t.description, inputSchema: t.inputSchema, ...(t.outputSchema ? { outputSchema: t.outputSchema } : {}), diff --git a/lib/docs/content/connect-claude.ts b/lib/docs/content/connect-claude.ts new file mode 100644 index 00000000..5f55d7c1 --- /dev/null +++ b/lib/docs/content/connect-claude.ts @@ -0,0 +1,78 @@ +export const CONNECT_CLAUDE_MD = `# Connect with Claude + +> Talk to your bookkeeping. Connect Accounted to Claude (claude.ai, Claude Desktop, or Claude Code) and ask questions, categorise transactions, and prepare a momsdeklaration in plain language — every write still stages for your approval first. + +Accounted ships an [MCP](https://modelcontextprotocol.io) server that exposes the full bookkeeping engine — 90+ tools — to any MCP client. The endpoint is: + +\`\`\` +https://gnubok.app/api/extensions/ext/mcp-server/mcp +\`\`\` + +There are two ways to connect, depending on your client. Both reach the same tools and the same approval model: read tools answer immediately, write tools (categorise, mark paid, create voucher, year-end) **stage a pending operation** that you confirm in chat or in the **/pending** web UI before anything is booked. + +## Path A — claude.ai / Claude Desktop custom connector (OAuth 2.1) + +Best for most users. No API key to manage — you authorise Accounted the same way you'd authorise any OAuth app. + +1. In **claude.ai** (Settings → Connectors) or **Claude Desktop** (Settings → Connectors → Add custom connector), choose **Add custom connector**. +2. Paste the connector URL: + \`\`\` + https://gnubok.app/api/extensions/ext/mcp-server/mcp + \`\`\` +3. Claude opens the Accounted OAuth 2.1 consent screen. Sign in and pick the company you want Claude to act on. +4. On the consent screen you grant **read-only scopes by default** (list invoices, read reports, compute VAT). Write scopes (create invoice, categorise, book vouchers, run year-end) are **listed separately and must be ticked explicitly** — leave them unchecked for a read-only review session. +5. Approve. Claude now lists the Accounted tools and you can start asking questions. + +Because the consent is per-company and scoped, you can connect a read-only key for a reviewer and a separate write-enabled connection for day-to-day bookkeeping. + +## Path B — \`npx gnubok-mcp\` with an API key (stdio bridge) + +Best for Claude Desktop on a machine where you'd rather use a long-lived API key than the OAuth flow, or for scripting. + +1. Mint an API key in the Accounted dashboard at **/settings/api**. Use a \`gnubok_sk_test_*\` key against the sandbox while you evaluate; switch to \`gnubok_sk_live_*\` for real data. +2. Add the stdio bridge to your \`claude_desktop_config.json\`: + \`\`\`json + { + "mcpServers": { + "gnubok": { + "command": "npx", + "args": ["gnubok-mcp"], + "env": { + "GNUBOK_API_KEY": "gnubok_sk_test_..." + } + } + } + } + \`\`\` +3. Restart Claude Desktop. The bridge proxies stdio JSON-RPC to the hosted MCP endpoint over HTTPS; the key carries the scopes you granted it at mint time. + +The key's scopes gate exactly which tools are callable — a key without write scopes can read reports and ledgers but cannot stage a booking. + +## Try these prompts + +All three run against the deterministic sandbox seed (use a \`gnubok_sk_test_*\` key or pick the sandbox company on the OAuth consent screen). They exercise the read path end-to-end without booking anything. + +1. **"Show my uncategorized bank transactions and suggest categories."** + Claude calls \`gnubok_list_uncategorized_transactions\` then \`gnubok_suggest_categories\` and walks you through the proposals. Approving one stages a \`gnubok_categorize_transaction\` pending operation — nothing is booked until you confirm. +2. **"Which invoices are overdue?"** + Claude calls \`gnubok_get_ar_ledger\` (kundreskontra) and lists outstanding customer invoices with aging. +3. **"Compute my VAT report for this quarter and tell me if I can close it."** + Claude calls \`gnubok_get_vat_report\` for the momsdeklaration rutor, then \`gnubok_vat_close_check\` to scan for blockers (uncategorised rows, unapproved supplier invoices, missing receipts on expenses ≥ 4 000 kr — the tool's high-value heuristic; BFL requires underlag for every affärshändelse regardless of amount) and reports \`ready_to_close\`. + +## 10-minute reviewer test + +A quick end-to-end pass to confirm the connection works before you trust it with real data. Run the steps in order; each lists what you do and what you should see. + +1. **Connect.** Use Path A (read-only scopes only) or Path B with a \`gnubok_sk_test_*\` key. → Claude lists the Accounted tools (titles like *List Uncategorized Transactions*, *VAT Declaration (Momsdeklaration)*). +2. **Confirm the company.** Ask *"Which company am I connected to?"* → Claude names the sandbox company (e.g. **Sandlådan Konsult**). +3. **Run prompt 1** (*uncategorized + suggest categories*). → A list of uncategorised rows plus category suggestions; no booking happens. +4. **Run prompt 2** (*overdue invoices*). → At least one overdue customer invoice with aging. +5. **Run prompt 3** (*VAT report + can I close*). → Momsdeklaration rutor returned; \`vat_close_check\` reports a **non-empty blocker list** (uncategorised transactions, an unapproved leverantörsfaktura, and a high-value business expense without a receipt). +6. **Stage a write.** Ask Claude to categorise one transaction. → Claude stages a pending operation and asks you to confirm — the booking does **not** post until you approve in chat or at **/pending**. + +If every step matches, the connector is wired correctly and the approval model is enforced. + +## Support + +Stuck connecting, or seeing an unexpected blocker? Use the in-app support form at **/help** — it routes straight to the product team with your company context attached. Include the client (claude.ai / Desktop / Code), the path you used (A or B), and the tool name from any error message. +` diff --git a/lib/docs/nav.ts b/lib/docs/nav.ts index 9ab4f516..2ad6d273 100644 --- a/lib/docs/nav.ts +++ b/lib/docs/nav.ts @@ -27,6 +27,7 @@ export const DOCS_NAV: DocsNavSection[] = [ { label: 'Introduction', href: '/docs/api', summary: 'What the Accounted REST API is and how to authenticate.' }, { label: 'Quickstart', href: '/docs/api/cookbook/quickstart', summary: 'Send your first invoice in five minutes.' }, { label: 'Authentication', href: '/docs/api#authentication', summary: 'API keys, scopes, test mode.' }, + { label: 'Connect with Claude', href: '/docs/api/connect-claude', summary: 'Connect Claude via the MCP server — OAuth connector or npx gnubok-mcp bridge.' }, ], }, { diff --git a/packages/gnubok-mcp/README.md b/packages/gnubok-mcp/README.md new file mode 100644 index 00000000..4eea605b --- /dev/null +++ b/packages/gnubok-mcp/README.md @@ -0,0 +1,56 @@ +# gnubok-mcp + +Connect [Claude Desktop](https://claude.ai/download) (or any stdio MCP client) to your [Accounted](https://gnubok.app) bookkeeping account. This is a thin stdio → HTTPS bridge: it forwards JSON-RPC over stdio to the hosted Accounted MCP server, which exposes 90+ bookkeeping tools (invoices, transactions, VAT/momsdeklaration, payroll, reports, year-end). + +Write tools stage a pending operation that you confirm before anything is booked — the bridge never books on its own. + +## Quickstart + +1. Mint an API key in the Accounted dashboard at **[/settings/api](https://app.gnubok.se/settings?tab=api)**. Use a `gnubok_sk_test_*` key against the sandbox while you evaluate; switch to `gnubok_sk_live_*` for real data. The key's scopes gate which tools are callable. + +2. Run the bridge with the key in the environment: + + ```bash + GNUBOK_API_KEY=gnubok_sk_test_... npx gnubok-mcp + ``` + + It reads JSON-RPC from stdin and writes responses to stdout, so you normally point an MCP client at it rather than running it by hand. + +## Claude Desktop config + +Add the bridge to your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "gnubok": { + "command": "npx", + "args": ["gnubok-mcp"], + "env": { + "GNUBOK_API_KEY": "gnubok_sk_test_..." + } + } + } +} +``` + +Restart Claude Desktop. The Accounted tools appear in the client and you can start asking questions like *"Show my uncategorized bank transactions and suggest categories."* + +### Environment variables + +| Variable | Required | Default | Purpose | +|---|---|---|---| +| `GNUBOK_API_KEY` | yes | — | Your `gnubok_sk_*` API key. | +| `GNUBOK_URL` | no | `https://app.gnubok.se/api/extensions/ext/mcp-server/mcp` | Override the MCP endpoint (e.g. for self-hosted Accounted). | + +## Alternative: claude.ai connector (no API key) + +If you use **claude.ai** or Claude Desktop's custom-connector flow, you can skip this bridge entirely and add Accounted as an OAuth 2.1 custom connector instead — paste the connector URL `https://gnubok.app/api/extensions/ext/mcp-server/mcp` and authorise on the Accounted consent screen (read-only scopes by default; write scopes are ticked explicitly). + +## Docs + +Full setup, sample prompts, and a 10-minute reviewer test: **[Connect with Claude](https://gnubok.app/docs/api/connect-claude)**. + +## License + +MIT diff --git a/packages/gnubok-mcp/package.json b/packages/gnubok-mcp/package.json index 9f0e79b4..f8090536 100644 --- a/packages/gnubok-mcp/package.json +++ b/packages/gnubok-mcp/package.json @@ -22,6 +22,7 @@ "node": ">=18" }, "files": [ - "index.mjs" + "index.mjs", + "README.md" ] }