e-arveldaja-mcp 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # e-arveldaja MCP Server
2
2
 
3
3
  TypeScript MCP server for the Estonian e-arveldaja (RIK e-Financials) REST API.
4
- 84 tools across 11 modules + 6 resources. Supports multiple companies/accounts.
4
+ 85 tools, 7 workflow prompts, 12 resources across 11 modules. Supports multiple companies/accounts.
5
5
 
6
6
  ## Quick Start
7
7
 
@@ -165,5 +165,5 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
165
165
  const transport = new StdioClientTransport({ command: "node", args: ["dist/index.js"] });
166
166
  const client = new Client({ name: "test", version: "1.0.0" });
167
167
  await client.connect(transport);
168
- const { tools } = await client.listTools(); // 82 tools
168
+ const { tools } = await client.listTools(); // 85 tools
169
169
  ```
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/e-arveldaja-mcp)](https://www.npmjs.com/package/e-arveldaja-mcp)
4
4
 
5
- MCP (Model Context Protocol) server for the Estonian e-arveldaja (RIK e-Financials) REST API. Works with any MCP-compatible AI assistant — Claude Code, Codex CLI, Gemini CLI, Google Antigravity, Cursor, Windsurf, Cline, and others.
5
+ MCP server for the Estonian e-arveldaja (RIK e-Financials) REST API. 85 tools, 7 workflow prompts, 12 resources. Works with any MCP client — Claude Code, Codex CLI, Gemini CLI, Cursor, Windsurf, Cline, and others.
6
6
 
7
7
  ## Disclaimer
8
8
 
@@ -31,31 +31,20 @@ For the demo server, set the environment variable `EARVELDAJA_SERVER=demo`.
31
31
 
32
32
  ## Setup
33
33
 
34
- ### Option A: npx (no install needed)
34
+ ### 1. Add the MCP server
35
35
 
36
- Just reference `npx e-arveldaja-mcp` in your MCP configno cloning or building required. See configuration examples below.
36
+ Most AI assistants can set this up for you just ask:
37
37
 
38
- ### Option B: From source
38
+ > "Add the e-arveldaja-mcp npm package as an MCP server"
39
39
 
40
+ If you prefer to do it manually:
41
+
42
+ **Claude Code:**
40
43
  ```bash
41
- git clone https://github.com/iseppo/e-arveldaja-mcp.git
42
- cd e-arveldaja-mcp
43
- npm install
44
- npm run build # tsc -> dist/
44
+ claude mcp add e-arveldaja -- npx -y e-arveldaja-mcp
45
45
  ```
46
46
 
47
- ### Connecting to your AI assistant
48
-
49
- This is a standard MCP server using stdio transport. Most AI assistants can set this up themselves — just ask:
50
-
51
- > "Add the e-arveldaja-mcp npm package as an MCP server to my configuration, using npx"
52
-
53
- The assistant will add `{"command": "npx", "args": ["-y", "e-arveldaja-mcp"]}` to its MCP config. No cloning or paths needed.
54
-
55
- If you prefer to configure manually:
56
-
57
- **JSON-based config** (Claude Code, Cursor, Windsurf, Cline, Gemini CLI, Antigravity):
58
-
47
+ **Other tools** (Cursor, Windsurf, Cline, Gemini CLI, Codex CLI, Antigravity) — add to your MCP config:
59
48
  ```json
60
49
  {
61
50
  "mcpServers": {
@@ -67,58 +56,65 @@ If you prefer to configure manually:
67
56
  }
68
57
  ```
69
58
 
70
- **TOML-based config** (Codex CLI):
71
-
72
- ```toml
73
- [mcp_servers.e-arveldaja]
74
- command = "npx"
75
- args = ["-y", "e-arveldaja-mcp"]
76
- ```
77
-
78
- If running from source, replace `"npx", "-y", "e-arveldaja-mcp"` with `"node", "/path/to/e-arveldaja-mcp/dist/index.js"`.
79
-
80
- Where this config file lives depends on your tool:
59
+ <details>
60
+ <summary>Config file locations by tool</summary>
81
61
 
82
62
  | Tool | Config file |
83
63
  |---|---|
84
64
  | **Claude Code** | `~/.claude/settings.json` or project `.claude/settings.json` |
85
- | **Codex CLI** | `~/.codex/config.toml` or project `.codex/config.toml` |
86
- | **Gemini CLI** | `~/.gemini/settings.json` or project `.gemini/settings.json` |
87
- | **Google Antigravity** | MCP Store UI → Manage MCP Servers → View raw config (`mcp_config.json`) |
65
+ | **Codex CLI** | `~/.codex/config.toml` (TOML format) |
66
+ | **Gemini CLI** | `~/.gemini/settings.json` |
67
+ | **Google Antigravity** | MCP Store UI → Manage MCP Servers → raw config |
88
68
  | **Cursor** | `.cursor/mcp.json` in your project |
89
69
  | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` |
90
70
  | **Cline** | VS Code settings under `cline.mcpServers` |
91
71
 
92
- See [CLAUDE.md](CLAUDE.md) for architecture details and full API documentation.
72
+ </details>
93
73
 
94
- ## Workflows
74
+ ### 2. Place your API key
95
75
 
96
- The project includes step-by-step workflow guides in [`workflows/`](workflows/) that orchestrate multiple MCP tools into complete accounting tasks. These work with any MCP client just paste the workflow into your AI assistant's prompt or follow the steps manually.
76
+ Put the downloaded `apikey.txt` in the working directory where you run your AI assistant. That's it the server finds it automatically.
97
77
 
98
- | Workflow | Description |
99
- |---|---|
100
- | [book-invoice](workflows/book-invoice.md) | Book a purchase invoice from PDF: extract data, validate, find/create supplier, suggest accounts, create invoice, upload PDF, confirm |
101
- | [reconcile-bank](workflows/reconcile-bank.md) | Match unconfirmed bank transactions to open invoices and confirm matches |
102
- | [month-end](workflows/month-end.md) | Run month-end close checklist: blockers, missing docs, duplicates, trial balance, P&L, balance sheet |
103
- | [new-supplier](workflows/new-supplier.md) | Create a supplier with Estonian business registry lookup and dedup check |
78
+ For multiple companies, place multiple files (`apikey.txt`, `apikey-company2.txt`, etc.) and use `list_connections` / `switch_connection` to switch between them.
104
79
 
105
- ### Claude Code slash commands
80
+ <details>
81
+ <summary>Alternative: environment variables</summary>
106
82
 
107
- If you use Claude Code, the same workflows are also available as slash commands in `.claude/commands/`. To install:
83
+ ```bash
84
+ export EARVELDAJA_API_KEY_ID=...
85
+ export EARVELDAJA_API_PUBLIC_VALUE=...
86
+ export EARVELDAJA_API_PASSWORD=...
87
+ ```
108
88
 
109
- **Option A:** Run Claude Code from the `e-arveldaja-mcp` directory — skills are auto-detected.
89
+ </details>
110
90
 
111
- **Option B:** Symlink or copy to your global commands:
91
+ <details>
92
+ <summary>Building from source</summary>
112
93
 
113
94
  ```bash
114
- # Symlink (stays up to date)
115
- ln -s /path/to/e-arveldaja-mcp/.claude/commands/*.md ~/.claude/commands/
116
-
117
- # Or copy
118
- cp /path/to/e-arveldaja-mcp/.claude/commands/*.md ~/.claude/commands/
95
+ git clone https://github.com/iseppo/e-arveldaja-mcp.git
96
+ cd e-arveldaja-mcp
97
+ npm install && npm run build
98
+ # Then use: "node", "/path/to/e-arveldaja-mcp/dist/index.js" instead of npx
119
99
  ```
120
100
 
121
- Then use `/book-invoice`, `/reconcile-bank`, `/month-end`, `/new-supplier` in any conversation.
101
+ </details>
102
+
103
+ ## Workflows (MCP Prompts)
104
+
105
+ The server includes 7 built-in workflow prompts that any MCP client can discover and use. These guide the AI assistant through multi-step accounting tasks:
106
+
107
+ | Prompt | Description |
108
+ |---|---|
109
+ | `book-invoice` | Book a purchase invoice from PDF: extract, validate, resolve supplier, create, upload, confirm |
110
+ | `reconcile-bank` | Match bank transactions to invoices, auto-confirm or review manually |
111
+ | `month-end-close` | Blockers, missing docs, duplicates, trial balance, P&L, balance sheet |
112
+ | `new-supplier` | Create supplier with Estonian business registry lookup |
113
+ | `company-overview` | Financial dashboard: balance sheet, P&L, receivables, payables |
114
+ | `quarterly-vat` | Prepare KMD (VAT return) data for a quarter |
115
+ | `lightyear-booking` | Book Lightyear investment trades and distributions from CSV |
116
+
117
+ **Claude Code** also has these as slash commands: `/book-invoice`, `/reconcile-bank`, `/month-end`, `/new-supplier`.
122
118
 
123
119
  ## Usage Examples
124
120
 
@@ -1,5 +1,6 @@
1
1
  import { Cache } from "../cache.js";
2
2
  import { log } from "../logger.js";
3
+ import { reportProgress } from "../progress.js";
3
4
  export const cache = new Cache(300);
4
5
  export class BaseResource {
5
6
  client;
@@ -38,6 +39,9 @@ export class BaseResource {
38
39
  if (totalPages > 1 && page === 1) {
39
40
  log("info", `${this.basePath}: fetching ${totalPages} pages...`);
40
41
  }
42
+ if (totalPages > 1) {
43
+ await reportProgress(page - 1, totalPages);
44
+ }
41
45
  page++;
42
46
  } while (page <= totalPages);
43
47
  return allItems;
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { z } from "zod";
6
6
  import { loadAllConfigs } from "./config.js";
7
+ import { toolExtraStorage } from "./progress.js";
7
8
  import { HttpClient } from "./http-client.js";
8
9
  import { ClientsApi } from "./api/clients.api.js";
9
10
  import { ProductsApi } from "./api/products.api.js";
@@ -92,7 +93,7 @@ async function main() {
92
93
  const api = createScopedApiContext(connectionState, connectionContexts, invocationStorage);
93
94
  const server = new McpServer({
94
95
  name: "e-arveldaja",
95
- version: "0.3.0",
96
+ version: "0.3.2",
96
97
  description: "EXPERIMENTAL, UNOFFICIAL MCP server for the Estonian e-arveldaja (e-Financials) API. " +
97
98
  "NOT affiliated with or endorsed by RIK. Use entirely at your own risk — " +
98
99
  "this software interacts with live financial data and can create, modify, and delete accounting records. " +
@@ -100,10 +101,27 @@ async function main() {
100
101
  "sale/purchase invoices. Includes account balance computation (D/C logic), " +
101
102
  "PDF invoice extraction, supplier resolution with business registry lookup, " +
102
103
  "and smart booking suggestions based on past invoices.",
104
+ }, {
105
+ instructions: `Purchase invoices:
106
+ - Before booking, call get_vat_info to check VAT registration status.
107
+ - Before creating, call detect_duplicate_purchase_invoice.
108
+ - Pass original vat_price and gross_price exactly — do not recalculate.
109
+ - Use list_purchase_articles to resolve cl_purchase_articles_id.
110
+ - For non-Estonian suppliers, check if reverse charge applies (reversed_vat_id=1).
111
+ - PDF flow: extract_pdf_invoice → validate_invoice_data → resolve_supplier → suggest_booking → create_purchase_invoice_from_pdf → upload_invoice_document → confirm_purchase_invoice.
112
+
113
+ Bank reconciliation:
114
+ - Run reconcile_transactions first, then auto_confirm_exact_matches with dry_run before executing.
115
+
116
+ Reporting:
117
+ - Confirm all journals/invoices/transactions first for accurate financial reports.
118
+ - list_connections / switch_connection for multi-company; switching clears caches.
119
+ - Batch tools default to dry_run — preview before execute=true.
120
+ - Amounts are EUR unless cl_currencies_id specifies otherwise.`,
103
121
  });
104
122
  // --- Multi-account tools ---
105
123
  server.tool("list_connections", "List all available e-arveldaja connections (API key files). " +
106
- "Shows which connection is currently active.", {}, readOnly, async () => {
124
+ "Shows which connection is currently active.", {}, { ...readOnly, title: "List Connections" }, async () => {
107
125
  const connections = allConfigs.map((nc, i) => ({
108
126
  index: i,
109
127
  name: nc.name,
@@ -126,7 +144,7 @@ async function main() {
126
144
  "Clears cached data atomically. Use list_connections to see available indices. " +
127
145
  "In-flight tool calls will fail fast and should be retried on the intended connection.", {
128
146
  index: z.number().describe("Connection index from list_connections"),
129
- }, mutate, async ({ index }) => {
147
+ }, { ...mutate, title: "Switch Connection" }, async ({ index }) => {
130
148
  if (index < 0 || index >= allConfigs.length) {
131
149
  return {
132
150
  content: [{
@@ -171,9 +189,13 @@ async function main() {
171
189
  function wrapHandler(handler) {
172
190
  return (async (...args) => {
173
191
  const snapshot = captureSnapshot(connectionState);
192
+ const extra = args.length >= 2 ? args[1] : undefined;
174
193
  try {
175
194
  return await invocationStorage.run(snapshot, async () => {
176
- const result = await handler(...args);
195
+ const runInExtra = extra
196
+ ? () => toolExtraStorage.run(extra, () => handler(...args))
197
+ : () => handler(...args);
198
+ const result = await runInExtra();
177
199
  assertSnapshotCurrent(connectionState, snapshot);
178
200
  return result;
179
201
  });
@@ -0,0 +1,13 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ export interface ToolExtra {
3
+ sendNotification: (notification: unknown) => Promise<void>;
4
+ _meta?: {
5
+ progressToken?: string | number;
6
+ };
7
+ }
8
+ export declare const toolExtraStorage: AsyncLocalStorage<ToolExtra>;
9
+ /**
10
+ * Report progress for long-running operations.
11
+ * No-op if the client didn't supply a progress token.
12
+ */
13
+ export declare function reportProgress(progress: number, total?: number): Promise<void>;
@@ -0,0 +1,24 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ export const toolExtraStorage = new AsyncLocalStorage();
3
+ /**
4
+ * Report progress for long-running operations.
5
+ * No-op if the client didn't supply a progress token.
6
+ */
7
+ export async function reportProgress(progress, total) {
8
+ const extra = toolExtraStorage.getStore();
9
+ if (!extra?._meta?.progressToken)
10
+ return;
11
+ try {
12
+ await extra.sendNotification({
13
+ method: "notifications/progress",
14
+ params: {
15
+ progressToken: extra._meta.progressToken,
16
+ progress,
17
+ ...(total !== undefined && { total }),
18
+ },
19
+ });
20
+ }
21
+ catch {
22
+ // Client may not support progress — ignore
23
+ }
24
+ }
@@ -62,16 +62,13 @@ async function computeAccountBalance(api, accountId, clientId, dateFrom, dateTo,
62
62
  };
63
63
  }
64
64
  export function registerAccountBalanceTools(server, api) {
65
- server.tool("compute_account_balance", "Compute account balance from journal postings (D/C logic). " +
66
- "For liability accounts (C-type): balance = credits - debits. " +
67
- "For asset accounts (D-type): balance = debits - credits. " +
68
- "Can filter by client and date range.", {
65
+ server.tool("compute_account_balance", "Compute an account balance from journal postings, with optional client and date filters. Applies the account's debit/credit direction automatically.", {
69
66
  account_id: z.number().describe("Account number (e.g. 2110 for short-term loans)"),
70
67
  client_id: z.number().optional().describe("Filter by client ID"),
71
68
  date_from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
72
69
  date_to: z.string().optional().describe("End date (YYYY-MM-DD)"),
73
70
  include_entries: z.boolean().optional().describe("Include individual entries in response (default false)"),
74
- }, readOnly, async ({ account_id, client_id, date_from, date_to, include_entries }) => {
71
+ }, { ...readOnly, title: "Compute Account Balance" }, async ({ account_id, client_id, date_from, date_to, include_entries }) => {
75
72
  const result = await computeAccountBalance(api, account_id, client_id, date_from, date_to);
76
73
  const summary = {
77
74
  account_id,
@@ -88,12 +85,10 @@ export function registerAccountBalanceTools(server, api) {
88
85
  };
89
86
  return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
90
87
  });
91
- server.tool("compute_client_debt", "Compute how much the company owes to a specific client (or vice versa). " +
92
- "Checks accounts 2110 (short-term loans), 2310 (accounts payable), 1210 (accounts receivable) by default. " +
93
- "Override account_ids for other accounts. Uses journal D/C postings.", {
88
+ server.tool("compute_client_debt", "Compute how much the company owes the client and vice versa across selected accounts (default: 2110, 2310, 1210). Uses journal D/C postings.", {
94
89
  client_id: z.number().describe("Client ID"),
95
90
  account_ids: z.string().optional().describe("Comma-separated account IDs to check (default: 2110,2310,1210)"),
96
- }, readOnly, async ({ client_id, account_ids }) => {
91
+ }, { ...readOnly, title: "Compute Client Net Position" }, async ({ client_id, account_ids }) => {
97
92
  const ids = account_ids
98
93
  ? account_ids.split(",").map(s => parseInt(s.trim()))
99
94
  : [2110, 2310, 1210]; // short-term loans, accounts payable, accounts receivable
@@ -27,7 +27,7 @@ export function registerAgingTools(server, api) {
27
27
  server.tool("compute_receivables_aging", "Compute receivables aging report (nõuete vanusanalüüs). " +
28
28
  "Groups unpaid sale invoices into aging buckets by client.", {
29
29
  as_of_date: z.string().optional().describe("Aging date (YYYY-MM-DD, default today)"),
30
- }, readOnly, async ({ as_of_date }) => {
30
+ }, { ...readOnly, title: "Receivables Aging Report" }, async ({ as_of_date }) => {
31
31
  const today = as_of_date ?? new Date().toISOString().split("T")[0];
32
32
  const usesDefaultUtcDate = !as_of_date;
33
33
  const allSales = await api.saleInvoices.listAll();
@@ -92,7 +92,7 @@ export function registerAgingTools(server, api) {
92
92
  server.tool("compute_payables_aging", "Compute payables aging report (kohustuste vanusanalüüs). " +
93
93
  "Groups unpaid purchase invoices into aging buckets by supplier.", {
94
94
  as_of_date: z.string().optional().describe("Aging date (YYYY-MM-DD, default today)"),
95
- }, readOnly, async ({ as_of_date }) => {
95
+ }, { ...readOnly, title: "Payables Aging Report" }, async ({ as_of_date }) => {
96
96
  const today = as_of_date ?? new Date().toISOString().split("T")[0];
97
97
  const usesDefaultUtcDate = !as_of_date;
98
98
  const allPurchases = await api.purchaseInvoices.listAll();
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { readOnly, batch } from "../annotations.js";
3
+ import { reportProgress } from "../progress.js";
3
4
  function matchScore(tx, invoice, txAmount) {
4
5
  let confidence = 0;
5
6
  const reasons = [];
@@ -48,7 +49,7 @@ export function registerBankReconciliationTools(server, api) {
48
49
  server.tool("reconcile_transactions", "Match unconfirmed bank transactions to open sale/purchase invoices. " +
49
50
  "Returns suggested matches with confidence scores and ready-to-use distribution data.", {
50
51
  min_confidence: z.number().optional().describe("Minimum confidence threshold 0-100 (default 50)"),
51
- }, readOnly, async ({ min_confidence }) => {
52
+ }, { ...readOnly, title: "Reconcile Transactions" }, async ({ min_confidence }) => {
52
53
  const threshold = min_confidence ?? 50;
53
54
  // Get all unconfirmed transactions
54
55
  const allTx = await api.transactions.listAll();
@@ -135,11 +136,10 @@ export function registerBankReconciliationTools(server, api) {
135
136
  }],
136
137
  };
137
138
  });
138
- server.tool("auto_confirm_exact_matches", "Automatically confirm bank transactions that have a single high-confidence match (>=90). " +
139
- "DRY RUN by default - set execute=true to actually confirm.", {
139
+ server.tool("auto_confirm_exact_matches", "Batch-confirm bank transactions with a single high-confidence match (>=90). DRY RUN by default — set execute=true to confirm.", {
140
140
  execute: z.boolean().optional().describe("Actually confirm transactions (default false = dry run)"),
141
141
  min_confidence: z.number().optional().describe("Minimum confidence (default 90)"),
142
- }, batch, async ({ execute, min_confidence }) => {
142
+ }, { ...batch, title: "Auto-Confirm Bank Matches" }, async ({ execute, min_confidence }) => {
143
143
  const threshold = min_confidence ?? 90;
144
144
  const dryRun = execute !== true;
145
145
  // Get all unconfirmed transactions across pages
@@ -153,7 +153,10 @@ export function registerBankReconciliationTools(server, api) {
153
153
  const skipped = [];
154
154
  // Track consumed invoices to avoid double-matching (keyed by type:id to prevent cross-table collisions)
155
155
  const consumedInvoiceKeys = new Set();
156
- for (const tx of unconfirmed) {
156
+ const total = unconfirmed.length;
157
+ for (let i = 0; i < unconfirmed.length; i++) {
158
+ const tx = unconfirmed[i];
159
+ await reportProgress(i, total);
157
160
  // Only process known transaction types
158
161
  if (tx.type !== "D" && tx.type !== "C") {
159
162
  skipped.push({ transaction_id: tx.id, reason: `Unknown transaction type "${tx.type}"` });
@@ -83,11 +83,11 @@ export function registerCrudTools(server, api) {
83
83
  // =====================
84
84
  // CLIENTS
85
85
  // =====================
86
- server.tool("list_clients", "List all clients (buyers/suppliers). Paginated.", pageParam.shape, readOnly, async (params) => {
86
+ server.tool("list_clients", "List all clients (buyers/suppliers). Paginated.", pageParam.shape, { ...readOnly, title: "List Clients" }, async (params) => {
87
87
  const result = await api.clients.list(params);
88
88
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
89
89
  });
90
- server.tool("get_client", "Get a single client by ID", idParam.shape, readOnly, async ({ id }) => {
90
+ server.tool("get_client", "Get a single client by ID", idParam.shape, { ...readOnly, title: "Get Client" }, async ({ id }) => {
91
91
  const result = await api.clients.get(id);
92
92
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
93
93
  });
@@ -105,7 +105,7 @@ export function registerCrudTools(server, api) {
105
105
  bank_account_no: z.string().optional().describe("Bank account (IBAN)"),
106
106
  invoice_vat_no: z.string().optional().describe("VAT number"),
107
107
  notes: z.string().optional().describe("Notes"),
108
- }, create, async (params) => {
108
+ }, { ...create, title: "Create Client" }, async (params) => {
109
109
  const result = await api.clients.create({
110
110
  ...params,
111
111
  cl_code_country: params.cl_code_country ?? "EST",
@@ -118,38 +118,38 @@ export function registerCrudTools(server, api) {
118
118
  server.tool("update_client", "Update an existing client", {
119
119
  id: z.number().describe("Client ID"),
120
120
  data: z.string().describe("JSON object with fields to update"),
121
- }, mutate, async ({ id, data }) => {
121
+ }, { ...mutate, title: "Update Client" }, async ({ id, data }) => {
122
122
  const result = await api.clients.update(id, parseJsonObject(data, "data"));
123
123
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
124
124
  });
125
- server.tool("deactivate_client", "Deactivate a client (can be restored with restore_client)", idParam.shape, mutate, async ({ id }) => {
125
+ server.tool("deactivate_client", "Deactivate a client (can be restored with restore_client)", idParam.shape, { ...mutate, title: "Deactivate Client" }, async ({ id }) => {
126
126
  const result = await api.clients.deactivate(id);
127
127
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
128
128
  });
129
- server.tool("restore_client", "Reactivate a deleted client", idParam.shape, mutate, async ({ id }) => {
129
+ server.tool("restore_client", "Reactivate a deactivated client", idParam.shape, { ...mutate, title: "Restore Client" }, async ({ id }) => {
130
130
  const result = await api.clients.restore(id);
131
131
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
132
132
  });
133
133
  server.tool("search_client", "Search clients by name (fuzzy match)", {
134
134
  name: z.string().describe("Name to search for"),
135
- }, readOnly, async ({ name }) => {
135
+ }, { ...readOnly, title: "Search Clients" }, async ({ name }) => {
136
136
  const results = await api.clients.findByName(name);
137
137
  return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
138
138
  });
139
- server.tool("find_client_by_code", "Find client by registry code", {
139
+ server.tool("find_client_by_code", "Find a client by business registry code or personal ID", {
140
140
  code: z.string().describe("Business registry code or personal ID"),
141
- }, readOnly, async ({ code }) => {
141
+ }, { ...readOnly, title: "Find Client by Registry Code" }, async ({ code }) => {
142
142
  const result = await api.clients.findByCode(code);
143
143
  return { content: [{ type: "text", text: result ? JSON.stringify(result, null, 2) : "Not found" }] };
144
144
  });
145
145
  // =====================
146
146
  // PRODUCTS
147
147
  // =====================
148
- server.tool("list_products", "List all products/services. Paginated.", pageParam.shape, readOnly, async (params) => {
148
+ server.tool("list_products", "List all products/services. Paginated.", pageParam.shape, { ...readOnly, title: "List Products" }, async (params) => {
149
149
  const result = await api.products.list(params);
150
150
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
151
151
  });
152
- server.tool("get_product", "Get a single product by ID", idParam.shape, readOnly, async ({ id }) => {
152
+ server.tool("get_product", "Get a single product by ID", idParam.shape, { ...readOnly, title: "Get Product" }, async ({ id }) => {
153
153
  const result = await api.products.get(id);
154
154
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
155
155
  });
@@ -160,33 +160,33 @@ export function registerCrudTools(server, api) {
160
160
  cl_purchase_articles_id: z.number().optional().describe("Purchase article ID"),
161
161
  sales_price: z.number().optional().describe("Sales price"),
162
162
  unit: z.string().optional().describe("Unit (e.g. tk, h, km)"),
163
- }, create, async (params) => {
163
+ }, { ...create, title: "Create Product" }, async (params) => {
164
164
  const result = await api.products.create(params);
165
165
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
166
166
  });
167
167
  server.tool("update_product", "Update a product", {
168
168
  id: z.number().describe("Product ID"),
169
169
  data: z.string().describe("JSON object with fields to update"),
170
- }, mutate, async ({ id, data }) => {
170
+ }, { ...mutate, title: "Update Product" }, async ({ id, data }) => {
171
171
  const result = await api.products.update(id, parseJsonObject(data, "data"));
172
172
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
173
173
  });
174
- server.tool("deactivate_product", "Deactivate a product (can be restored with restore_product)", idParam.shape, mutate, async ({ id }) => {
174
+ server.tool("deactivate_product", "Deactivate a product (can be restored with restore_product)", idParam.shape, { ...mutate, title: "Deactivate Product" }, async ({ id }) => {
175
175
  const result = await api.products.deactivate(id);
176
176
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
177
177
  });
178
- server.tool("restore_product", "Reactivate a deleted product", idParam.shape, mutate, async ({ id }) => {
178
+ server.tool("restore_product", "Reactivate a deactivated product", idParam.shape, { ...mutate, title: "Restore Product" }, async ({ id }) => {
179
179
  const result = await api.products.restore(id);
180
180
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
181
181
  });
182
182
  // =====================
183
183
  // JOURNALS
184
184
  // =====================
185
- server.tool("list_journals", "List journal entries. Paginated.", pageParam.shape, readOnly, async (params) => {
185
+ server.tool("list_journals", "List journal entries. Paginated.", pageParam.shape, { ...readOnly, title: "List Journals" }, async (params) => {
186
186
  const result = await api.journals.list(params);
187
187
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
188
188
  });
189
- server.tool("get_journal", "Get a journal entry by ID (includes postings)", idParam.shape, readOnly, async ({ id }) => {
189
+ server.tool("get_journal", "Get a journal entry by ID (includes postings)", idParam.shape, { ...readOnly, title: "Get Journal" }, async ({ id }) => {
190
190
  const result = await api.journals.get(id);
191
191
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
192
192
  });
@@ -197,7 +197,7 @@ export function registerCrudTools(server, api) {
197
197
  document_number: z.string().optional().describe("Document number"),
198
198
  cl_currencies_id: z.string().optional().describe("Currency (default EUR)"),
199
199
  postings: z.string().describe("JSON array of postings: [{accounts_id, type: 'D'|'C', amount, accounts_dimensions_id?, ...}]"),
200
- }, create, async (params) => {
200
+ }, { ...create, title: "Create Journal" }, async (params) => {
201
201
  const result = await api.journals.create({
202
202
  ...params,
203
203
  cl_currencies_id: params.cl_currencies_id ?? "EUR",
@@ -208,30 +208,30 @@ export function registerCrudTools(server, api) {
208
208
  server.tool("update_journal", "Update a journal entry", {
209
209
  id: z.number().describe("Journal ID"),
210
210
  data: z.string().describe("JSON object with fields to update"),
211
- }, mutate, async ({ id, data }) => {
211
+ }, { ...mutate, title: "Update Journal" }, async ({ id, data }) => {
212
212
  const result = await api.journals.update(id, parseJsonObject(data, "data"));
213
213
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
214
214
  });
215
- server.tool("delete_journal", "Delete a journal entry", idParam.shape, destructive, async ({ id }) => {
215
+ server.tool("delete_journal", "Delete a journal entry", idParam.shape, { ...destructive, title: "Delete Journal" }, async ({ id }) => {
216
216
  const result = await api.journals.delete(id);
217
217
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
218
218
  });
219
- server.tool("confirm_journal", "Confirm/register a journal entry. IRREVERSIBLE — use invalidate_journal to reverse if needed.", idParam.shape, destructive, async ({ id }) => {
219
+ server.tool("confirm_journal", "Confirm/register a journal entry. IRREVERSIBLE — use invalidate_journal to reverse if needed.", idParam.shape, { ...destructive, title: "Confirm Journal" }, async ({ id }) => {
220
220
  const result = await api.journals.confirm(id);
221
221
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
222
222
  });
223
- server.tool("invalidate_journal", "Invalidate (reverse) a confirmed journal entry. Returns it to unconfirmed status for editing or deletion.", idParam.shape, mutate, async ({ id }) => {
223
+ server.tool("invalidate_journal", "Invalidate (reverse) a confirmed journal entry. Returns it to unconfirmed status for editing or deletion.", idParam.shape, { ...mutate, title: "Invalidate Journal" }, async ({ id }) => {
224
224
  const result = await api.journals.invalidate(id);
225
225
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
226
226
  });
227
227
  // =====================
228
228
  // TRANSACTIONS
229
229
  // =====================
230
- server.tool("list_transactions", "List bank transactions. Paginated.", pageParam.shape, readOnly, async (params) => {
230
+ server.tool("list_transactions", "List bank transactions. Paginated.", pageParam.shape, { ...readOnly, title: "List Transactions" }, async (params) => {
231
231
  const result = await api.transactions.list(params);
232
232
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
233
233
  });
234
- server.tool("get_transaction", "Get a transaction by ID", idParam.shape, readOnly, async ({ id }) => {
234
+ server.tool("get_transaction", "Get a transaction by ID", idParam.shape, { ...readOnly, title: "Get Transaction" }, async ({ id }) => {
235
235
  const result = await api.transactions.get(id);
236
236
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
237
237
  });
@@ -245,37 +245,37 @@ export function registerCrudTools(server, api) {
245
245
  clients_id: z.number().optional().describe("Related client ID"),
246
246
  bank_account_name: z.string().optional().describe("Remitter/beneficiary name"),
247
247
  ref_number: z.string().optional().describe("Reference number"),
248
- }, create, async (params) => {
248
+ }, { ...create, title: "Create Transaction" }, async (params) => {
249
249
  const result = await api.transactions.create({
250
250
  ...params,
251
251
  cl_currencies_id: params.cl_currencies_id ?? "EUR",
252
252
  });
253
253
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
254
254
  });
255
- server.tool("confirm_transaction", "Confirm a transaction with distribution rows", {
255
+ server.tool("confirm_transaction", "Confirm a bank transaction by providing distribution rows", {
256
256
  id: z.number().describe("Transaction ID"),
257
257
  distributions: z.string().optional().describe("JSON array of distribution rows: [{related_table, related_id?, amount}]"),
258
- }, destructive, async ({ id, distributions }) => {
258
+ }, { ...destructive, title: "Confirm Transaction" }, async ({ id, distributions }) => {
259
259
  const dist = distributions ? parseTransactionDistributions(distributions) : undefined;
260
260
  const result = await api.transactions.confirm(id, dist);
261
261
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
262
262
  });
263
- server.tool("invalidate_transaction", "Invalidate (unconfirm) a confirmed transaction. Returns it to unconfirmed status for editing or deletion.", idParam.shape, mutate, async ({ id }) => {
263
+ server.tool("invalidate_transaction", "Invalidate (unconfirm) a confirmed transaction. Returns it to unconfirmed status for editing or deletion.", idParam.shape, { ...mutate, title: "Invalidate Transaction" }, async ({ id }) => {
264
264
  const result = await api.transactions.invalidate(id);
265
265
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
266
266
  });
267
- server.tool("delete_transaction", "Delete a transaction", idParam.shape, destructive, async ({ id }) => {
267
+ server.tool("delete_transaction", "Delete a transaction", idParam.shape, { ...destructive, title: "Delete Transaction" }, async ({ id }) => {
268
268
  const result = await api.transactions.delete(id);
269
269
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
270
270
  });
271
271
  // =====================
272
272
  // SALE INVOICES
273
273
  // =====================
274
- server.tool("list_sale_invoices", "List sales invoices. Paginated.", pageParam.shape, readOnly, async (params) => {
274
+ server.tool("list_sale_invoices", "List sales invoices. Paginated.", pageParam.shape, { ...readOnly, title: "List Sale Invoices" }, async (params) => {
275
275
  const result = await api.saleInvoices.list(params);
276
276
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
277
277
  });
278
- server.tool("get_sale_invoice", "Get a sales invoice by ID (includes items, deliveries)", idParam.shape, readOnly, async ({ id }) => {
278
+ server.tool("get_sale_invoice", "Get a sales invoice by ID (includes items, deliveries)", idParam.shape, { ...readOnly, title: "Get Sale Invoice" }, async ({ id }) => {
279
279
  const result = await api.saleInvoices.get(id);
280
280
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
281
281
  });
@@ -292,7 +292,7 @@ export function registerCrudTools(server, api) {
292
292
  show_client_balance: z.boolean().optional().describe("Show client balance on invoice"),
293
293
  items: z.string().describe("JSON array of invoice items: [{products_id, custom_title, amount, unit_net_price, ...}]"),
294
294
  notes: z.string().optional().describe("Internal notes"),
295
- }, create, async (params) => {
295
+ }, { ...create, title: "Create Sale Invoice" }, async (params) => {
296
296
  const result = await api.saleInvoices.create({
297
297
  ...params,
298
298
  number_suffix: params.number_suffix ?? "",
@@ -307,19 +307,19 @@ export function registerCrudTools(server, api) {
307
307
  server.tool("update_sale_invoice", "Update a sales invoice", {
308
308
  id: z.number().describe("Invoice ID"),
309
309
  data: z.string().describe("JSON with fields to update"),
310
- }, mutate, async ({ id, data }) => {
310
+ }, { ...mutate, title: "Update Sale Invoice" }, async ({ id, data }) => {
311
311
  const result = await api.saleInvoices.update(id, parseJsonObject(data, "data"));
312
312
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
313
313
  });
314
- server.tool("delete_sale_invoice", "Delete a sales invoice", idParam.shape, destructive, async ({ id }) => {
314
+ server.tool("delete_sale_invoice", "Delete a sales invoice", idParam.shape, { ...destructive, title: "Delete Sale Invoice" }, async ({ id }) => {
315
315
  const result = await api.saleInvoices.delete(id);
316
316
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
317
317
  });
318
- server.tool("confirm_sale_invoice", "Confirm a sales invoice. IRREVERSIBLE — locks the invoice for editing.", idParam.shape, destructive, async ({ id }) => {
318
+ server.tool("confirm_sale_invoice", "Confirm a sales invoice. IRREVERSIBLE — locks the invoice for editing.", idParam.shape, { ...destructive, title: "Confirm Sale Invoice" }, async ({ id }) => {
319
319
  const result = await api.saleInvoices.confirm(id);
320
320
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
321
321
  });
322
- server.tool("get_sale_invoice_delivery_options", "Get delivery options for a sales invoice", idParam.shape, readOnly, async ({ id }) => {
322
+ server.tool("get_sale_invoice_delivery_options", "Get available delivery methods for a sales invoice (e-invoice or email)", idParam.shape, { ...readOnly, title: "Get Sale Invoice Delivery Options" }, async ({ id }) => {
323
323
  const result = await api.saleInvoices.getDeliveryOptions(id);
324
324
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
325
325
  });
@@ -330,29 +330,26 @@ export function registerCrudTools(server, api) {
330
330
  email_addresses: z.string().optional().describe("Email addresses"),
331
331
  email_subject: z.string().optional().describe("Email subject"),
332
332
  email_body: z.string().optional().describe("Email body"),
333
- }, send, async ({ id, ...request }) => {
333
+ }, { ...send, title: "Send Sale Invoice" }, async ({ id, ...request }) => {
334
334
  const result = await api.saleInvoices.sendEinvoice(id, request);
335
335
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
336
336
  });
337
- server.tool("get_sale_invoice_document", "Download sales invoice PDF (base64)", idParam.shape, readOnly, async ({ id }) => {
337
+ server.tool("get_sale_invoice_document", "Download sales invoice PDF (base64)", idParam.shape, { ...readOnly, title: "Download Invoice PDF" }, async ({ id }) => {
338
338
  const result = await api.saleInvoices.getDocument(id);
339
339
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
340
340
  });
341
341
  // =====================
342
342
  // PURCHASE INVOICES
343
343
  // =====================
344
- server.tool("list_purchase_invoices", "List purchase invoices. Paginated.", pageParam.shape, readOnly, async (params) => {
344
+ server.tool("list_purchase_invoices", "List purchase invoices. Paginated.", pageParam.shape, { ...readOnly, title: "List Purchase Invoices" }, async (params) => {
345
345
  const result = await api.purchaseInvoices.list(params);
346
346
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
347
347
  });
348
- server.tool("get_purchase_invoice", "Get a purchase invoice by ID", idParam.shape, readOnly, async ({ id }) => {
348
+ server.tool("get_purchase_invoice", "Get a purchase invoice by ID", idParam.shape, { ...readOnly, title: "Get Purchase Invoice" }, async ({ id }) => {
349
349
  const result = await api.purchaseInvoices.get(id);
350
350
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
351
351
  });
352
- server.tool("create_purchase_invoice", "Create a purchase invoice. Pass the EXACT vat_price and gross_price from the original invoice " +
353
- "to ensure amounts match for payment reconciliation. " +
354
- "Items require cl_purchase_articles_id (use list_purchase_articles). " +
355
- "cl_fringe_benefits_id defaults to 1 (not a fringe benefit).", {
352
+ server.tool("create_purchase_invoice", "Create a draft purchase invoice with line items. Requires cl_purchase_articles_id (use list_purchase_articles). Pass EXACT vat_price and gross_price from the original invoice.", {
356
353
  clients_id: z.number().describe("Supplier client ID"),
357
354
  client_name: z.string().describe("Supplier name"),
358
355
  number: z.string().describe("Invoice number"),
@@ -367,7 +364,7 @@ export function registerCrudTools(server, api) {
367
364
  notes: z.string().optional().describe("Notes"),
368
365
  bank_ref_number: z.string().optional().describe("Payment reference number"),
369
366
  bank_account_no: z.string().optional().describe("Supplier bank account"),
370
- }, create, async (params) => {
367
+ }, { ...create, title: "Create Purchase Invoice" }, async (params) => {
371
368
  const isVatReg = await isCompanyVatRegistered(api);
372
369
  const purchaseArticles = await getPurchaseArticlesWithVat(api);
373
370
  const rawItems = parsePurchaseInvoiceItems(params.items);
@@ -391,65 +388,64 @@ export function registerCrudTools(server, api) {
391
388
  server.tool("update_purchase_invoice", "Update a purchase invoice", {
392
389
  id: z.number().describe("Invoice ID"),
393
390
  data: z.string().describe("JSON with fields to update"),
394
- }, mutate, async ({ id, data }) => {
391
+ }, { ...mutate, title: "Update Purchase Invoice" }, async ({ id, data }) => {
395
392
  const result = await api.purchaseInvoices.update(id, parseJsonObject(data, "data"));
396
393
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
397
394
  });
398
- server.tool("delete_purchase_invoice", "Delete a purchase invoice", idParam.shape, destructive, async ({ id }) => {
395
+ server.tool("delete_purchase_invoice", "Delete a purchase invoice", idParam.shape, { ...destructive, title: "Delete Purchase Invoice" }, async ({ id }) => {
399
396
  const result = await api.purchaseInvoices.delete(id);
400
397
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
401
398
  });
402
- server.tool("confirm_purchase_invoice", "Confirm a purchase invoice. IRREVERSIBLE locks the invoice for editing. " +
403
- "Automatically fixes vat_price/gross_price if they are missing or inconsistent with the item totals.", idParam.shape, destructive, async ({ id }) => {
399
+ server.tool("confirm_purchase_invoice", "Confirm and lock a purchase invoice. Automatically fixes vat_price/gross_price if missing or inconsistent with item totals.", idParam.shape, { ...destructive, title: "Confirm Purchase Invoice" }, async ({ id }) => {
404
400
  const isVatReg = await isCompanyVatRegistered(api);
405
401
  const result = await api.purchaseInvoices.confirmWithTotals(id, isVatReg);
406
402
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
407
403
  });
408
- server.tool("invalidate_purchase_invoice", "Invalidate (reverse) a confirmed purchase invoice. Returns it to PROJECT status for editing.", idParam.shape, mutate, async ({ id }) => {
404
+ server.tool("invalidate_purchase_invoice", "Return a confirmed purchase invoice to draft status for editing.", idParam.shape, { ...mutate, title: "Invalidate Purchase Invoice" }, async ({ id }) => {
409
405
  const result = await api.purchaseInvoices.invalidate(id);
410
406
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
411
407
  });
412
408
  // =====================
413
409
  // REFERENCE DATA (read-only)
414
410
  // =====================
415
- server.tool("list_accounts", "Get chart of accounts (kontoplaani kontod)", {}, readOnly, async () => {
411
+ server.tool("list_accounts", "Get chart of accounts (kontoplaani kontod)", {}, { ...readOnly, title: "List Accounts" }, async () => {
416
412
  const result = await api.readonly.getAccounts();
417
413
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
418
414
  });
419
- server.tool("list_account_dimensions", "Get account dimensions (alamkontod)", {}, readOnly, async () => {
415
+ server.tool("list_account_dimensions", "Get account dimensions (alamkontod)", {}, { ...readOnly, title: "List Account Dimensions" }, async () => {
420
416
  const result = await api.readonly.getAccountDimensions();
421
417
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
422
418
  });
423
- server.tool("list_currencies", "Get available currencies", {}, readOnly, async () => {
419
+ server.tool("list_currencies", "Get available currencies", {}, { ...readOnly, title: "List Currencies" }, async () => {
424
420
  const result = await api.readonly.getCurrencies();
425
421
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
426
422
  });
427
- server.tool("list_sale_articles", "Get sales articles (müügiartiklid)", {}, readOnly, async () => {
423
+ server.tool("list_sale_articles", "Get sales articles (müügiartiklid)", {}, { ...readOnly, title: "List Sale Articles" }, async () => {
428
424
  const result = await api.readonly.getSaleArticles();
429
425
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
430
426
  });
431
- server.tool("list_purchase_articles", "Get purchase articles (ostuartiklid)", {}, readOnly, async () => {
427
+ server.tool("list_purchase_articles", "Get purchase articles (ostuartiklid)", {}, { ...readOnly, title: "List Purchase Articles" }, async () => {
432
428
  const result = await api.readonly.getPurchaseArticles();
433
429
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
434
430
  });
435
- server.tool("list_templates", "Get sales invoice templates", {}, readOnly, async () => {
431
+ server.tool("list_templates", "Get sales invoice templates", {}, { ...readOnly, title: "List Invoice Templates" }, async () => {
436
432
  const result = await api.readonly.getTemplates();
437
433
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
438
434
  });
439
- server.tool("list_projects", "Get cost/profit centers (projektid)", {}, readOnly, async () => {
435
+ server.tool("list_projects", "Get cost/profit centers (projektid)", {}, { ...readOnly, title: "List Projects" }, async () => {
440
436
  const result = await api.readonly.getProjects();
441
437
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
442
438
  });
443
- server.tool("get_invoice_info", "Get company invoice settings", {}, readOnly, async () => {
439
+ server.tool("get_invoice_info", "Get company invoice settings", {}, { ...readOnly, title: "Get Invoice Settings" }, async () => {
444
440
  const result = await api.readonly.getInvoiceInfo();
445
441
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
446
442
  });
447
- server.tool("get_vat_info", "Get company VAT information (KMKR)", {}, readOnly, async () => {
443
+ server.tool("get_vat_info", "Get company VAT information (KMKR)", {}, { ...readOnly, title: "Get VAT Info" }, async () => {
448
444
  const result = await api.readonly.getVatInfo();
449
445
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
450
446
  });
451
447
  // Invoice series CRUD
452
- server.tool("list_invoice_series", "Get invoice numbering series", {}, readOnly, async () => {
448
+ server.tool("list_invoice_series", "Get invoice numbering series", {}, { ...readOnly, title: "List Invoice Series" }, async () => {
453
449
  const result = await api.readonly.getInvoiceSeries();
454
450
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
455
451
  });
@@ -460,12 +456,12 @@ export function registerCrudTools(server, api) {
460
456
  is_active: z.boolean().describe("Is active"),
461
457
  is_default: z.boolean().describe("Is default series"),
462
458
  overdue_charge: z.number().optional().describe("Delinquency charge per day"),
463
- }, create, async (params) => {
459
+ }, { ...create, title: "Create Invoice Series" }, async (params) => {
464
460
  const result = await api.readonly.createInvoiceSeries(params);
465
461
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
466
462
  });
467
463
  // Bank accounts CRUD
468
- server.tool("list_bank_accounts", "Get company bank accounts", {}, readOnly, async () => {
464
+ server.tool("list_bank_accounts", "Get company bank accounts", {}, { ...readOnly, title: "List Bank Accounts" }, async () => {
469
465
  const result = await api.readonly.getBankAccounts();
470
466
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
471
467
  });
@@ -475,7 +471,7 @@ export function registerCrudTools(server, api) {
475
471
  cl_banks_id: z.number().optional().describe("Bank ID"),
476
472
  swift_code: z.string().optional().describe("SWIFT/BIC code"),
477
473
  show_in_sale_invoices: z.boolean().optional().describe("Show on invoices"),
478
- }, create, async (params) => {
474
+ }, { ...create, title: "Create Bank Account" }, async (params) => {
479
475
  const result = await api.readonly.createBankAccount(params);
480
476
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
481
477
  });
@@ -5,7 +5,7 @@ export function registerDocumentAuditTools(server, api) {
5
5
  "Important for audit trail compliance.", {
6
6
  date_from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
7
7
  date_to: z.string().optional().describe("End date (YYYY-MM-DD)"),
8
- }, readOnly, async ({ date_from, date_to }) => {
8
+ }, { ...readOnly, title: "Find Missing Documents" }, async ({ date_from, date_to }) => {
9
9
  // Journals without documents
10
10
  const allJournals = await api.journals.listAll();
11
11
  const journalsWithout = allJournals.filter(j => {
@@ -82,11 +82,11 @@ export function registerDocumentAuditTools(server, api) {
82
82
  }],
83
83
  };
84
84
  });
85
- server.tool("detect_duplicate_purchase_invoice", "Check for potential duplicate purchase invoices based on supplier, invoice number, amount, and date.", {
85
+ server.tool("detect_duplicate_purchase_invoice", "Check for duplicate purchase invoices by supplier + invoice number, and by supplier + amount + date.", {
86
86
  clients_id: z.number().optional().describe("Filter by supplier ID"),
87
87
  date_from: z.string().optional().describe("Start date"),
88
88
  date_to: z.string().optional().describe("End date"),
89
- }, readOnly, async ({ clients_id, date_from, date_to }) => {
89
+ }, { ...readOnly, title: "Detect Duplicate Purchase Invoices" }, async ({ clients_id, date_from, date_to }) => {
90
90
  const allPurchases = await api.purchaseInvoices.listAll();
91
91
  const filtered = allPurchases.filter((inv) => {
92
92
  if (inv.status === "DELETED" || inv.status === "INVALIDATED")
@@ -48,10 +48,7 @@ async function computeRetainedEarningsBalance(api, accountId, asOfDate) {
48
48
  return roundMoney(credit - debit);
49
49
  }
50
50
  export function registerEstonianTaxTools(server, api) {
51
- server.tool("prepare_dividend_package", "Calculate dividend distribution and create draft journal entries. " +
52
- "Estonian CIT on dividends: 22/78 (from 2025). " +
53
- "Creates a debit to retained earnings and credit to payable + tax liability. " +
54
- "Validates accounts exist and checks retained earnings balance before posting.", {
51
+ server.tool("prepare_dividend_package", "Calculate dividend tax (22/78 CIT) and create draft journal entries for dividend payable and tax liability. Validates retained earnings balance and net assets.", {
55
52
  net_dividend: z.number().describe("Net dividend amount to shareholder (EUR)"),
56
53
  shareholder_client_id: z.number().describe("Shareholder client ID"),
57
54
  effective_date: z.string().describe("Distribution date (YYYY-MM-DD)"),
@@ -60,7 +57,7 @@ export function registerEstonianTaxTools(server, api) {
60
57
  tax_payable_account: z.number().optional().describe("CIT payable account (default 2540)"),
61
58
  share_capital_account: z.number().optional().describe("Share capital account for ÄS §157 net-assets check (default 3000)"),
62
59
  force: z.boolean().optional().describe("Create journal even if retained earnings are insufficient (default false)"),
63
- }, create, async ({ net_dividend, shareholder_client_id, effective_date, retained_earnings_account, dividend_payable_account, tax_payable_account, share_capital_account, force }) => {
60
+ }, { ...create, title: "Prepare Dividend Distribution" }, async ({ net_dividend, shareholder_client_id, effective_date, retained_earnings_account, dividend_payable_account, tax_payable_account, share_capital_account, force }) => {
64
61
  const retainedAccount = retained_earnings_account ?? 3020;
65
62
  const payableAccount = dividend_payable_account ?? 2370;
66
63
  const taxAccount = tax_payable_account ?? 2540;
@@ -172,9 +169,7 @@ export function registerEstonianTaxTools(server, api) {
172
169
  }],
173
170
  };
174
171
  });
175
- server.tool("create_owner_expense_reimbursement", "Book an owner-paid business expense: expense account + VAT + payable to owner. " +
176
- "Common for micro-OÜs where the owner pays with personal funds. " +
177
- "Books input VAT separately only for VAT-registered companies and validates accounts in chart of accounts.", {
172
+ server.tool("create_owner_expense_reimbursement", "Create a journal for a business expense paid personally by the owner. Splits input VAT for VAT-registered companies.", {
178
173
  owner_client_id: z.number().describe("Owner/shareholder client ID"),
179
174
  effective_date: z.string().describe("Expense date (YYYY-MM-DD)"),
180
175
  description: z.string().describe("Expense description"),
@@ -185,7 +180,7 @@ export function registerEstonianTaxTools(server, api) {
185
180
  vat_account: z.number().optional().describe("Input VAT account (default 1510)"),
186
181
  payable_account: z.number().optional().describe("Payable to owner account (default 2110)"),
187
182
  document_number: z.string().optional().describe("Receipt/document number"),
188
- }, create, async ({ owner_client_id, effective_date, description, net_amount, vat_rate, vat_amount, expense_account, vat_account, payable_account, document_number }) => {
183
+ }, { ...create, title: "Book Owner-Paid Expense" }, async ({ owner_client_id, effective_date, description, net_amount, vat_rate, vat_amount, expense_account, vat_account, payable_account, document_number }) => {
189
184
  const vatRegistered = await isCompanyVatRegistered(api);
190
185
  const vatAcc = vat_account ?? 1510;
191
186
  const payAcc = payable_account ?? 2110;
@@ -80,7 +80,7 @@ export function registerFinancialStatementTools(server, api) {
80
80
  "Shows debit/credit totals and balance for each account.", {
81
81
  date_from: z.string().optional().describe("Period start (YYYY-MM-DD)"),
82
82
  date_to: z.string().optional().describe("Period end (YYYY-MM-DD)"),
83
- }, readOnly, async ({ date_from, date_to }) => {
83
+ }, { ...readOnly, title: "Compute Trial Balance" }, async ({ date_from, date_to }) => {
84
84
  const balances = await computeAllBalances(api, date_from, date_to);
85
85
  const totalDebit = balances.reduce((s, b) => s + b.debit_total, 0);
86
86
  const totalCredit = balances.reduce((s, b) => s + b.credit_total, 0);
@@ -103,7 +103,7 @@ export function registerFinancialStatementTools(server, api) {
103
103
  server.tool("compute_balance_sheet", "Compute balance sheet (bilanss) from journal postings. " +
104
104
  "Groups accounts into Varad (Assets) and Kohustused+Omakapital (Liabilities+Equity).", {
105
105
  date_to: z.string().optional().describe("Balance sheet date (YYYY-MM-DD, default: today)"),
106
- }, readOnly, async ({ date_to }) => {
106
+ }, { ...readOnly, title: "Compute Balance Sheet" }, async ({ date_to }) => {
107
107
  const balances = await computeAllBalances(api, undefined, date_to);
108
108
  const assets = balances.filter(b => b.account_type_est === "Varad");
109
109
  const liabilities = balances.filter(b => b.account_type_est === "Kohustused");
@@ -160,7 +160,7 @@ export function registerFinancialStatementTools(server, api) {
160
160
  "Shows revenue minus expenses.", {
161
161
  date_from: z.string().describe("Period start (YYYY-MM-DD)"),
162
162
  date_to: z.string().describe("Period end (YYYY-MM-DD)"),
163
- }, readOnly, async ({ date_from, date_to }) => {
163
+ }, { ...readOnly, title: "Compute Profit and Loss" }, async ({ date_from, date_to }) => {
164
164
  const balances = await computeAllBalances(api, date_from, date_to);
165
165
  const revenue = balances.filter(b => b.account_type_est === "Tulud");
166
166
  const expenses = balances.filter(b => b.account_type_est === "Kulud");
@@ -187,7 +187,7 @@ export function registerFinancialStatementTools(server, api) {
187
187
  server.tool("month_end_close_checklist", "Generate month-end close checklist: unconfirmed journals/invoices, " +
188
188
  "unreconciled bank transactions, overdue receivables/payables.", {
189
189
  month: z.string().describe("Month to check (YYYY-MM, e.g. 2026-02)"),
190
- }, readOnly, async ({ month }) => {
190
+ }, { ...readOnly, title: "Month-End Close Checklist" }, async ({ month }) => {
191
191
  const dateFrom = `${month}-01`;
192
192
  const lastDay = getMonthLastDay(month);
193
193
  const dateTo = `${month}-${String(lastDay).padStart(2, "0")}`;
@@ -3,6 +3,7 @@ import { readFile } from "fs/promises";
3
3
  import { validateFilePath } from "../file-validation.js";
4
4
  import { roundMoney } from "../money.js";
5
5
  import { readOnly, batch } from "../annotations.js";
6
+ import { reportProgress } from "../progress.js";
6
7
  const MAX_CSV_SIZE = 10 * 1024 * 1024; // 10 MB
7
8
  // BRICEKSP is Lightyear's money market cash fund - not a real investment
8
9
  const CASH_FUND_TICKER = "BRICEKSP";
@@ -304,7 +305,7 @@ export function registerLightyearTools(server, api) {
304
305
  "distributions, deposits, withdrawals. Filters out BRICEKSP money market fund trades. " +
305
306
  "Pairs foreign currency trades with their FX conversion entries.", {
306
307
  file_path: z.string().describe("Absolute path to Lightyear AccountStatement CSV file"),
307
- }, readOnly, async ({ file_path }) => {
308
+ }, { ...readOnly, title: "Parse Lightyear Account Statement" }, async ({ file_path }) => {
308
309
  const csv = await readCsvFile(file_path);
309
310
  const rows = parseAccountStatement(csv);
310
311
  const { trades, warnings: fxWarnings } = extractTrades(rows);
@@ -377,7 +378,7 @@ export function registerLightyearTools(server, api) {
377
378
  server.tool("parse_lightyear_capital_gains", "Parse a Lightyear Capital Gains Statement CSV (FIFO method). " +
378
379
  "Shows cost basis, proceeds, and realized capital gains per sale.", {
379
380
  file_path: z.string().describe("Absolute path to Lightyear CapitalGainsStatement CSV file"),
380
- }, readOnly, async ({ file_path }) => {
381
+ }, { ...readOnly, title: "Parse Lightyear Capital Gains" }, async ({ file_path }) => {
381
382
  const csv = await readCsvFile(file_path);
382
383
  const gains = parseCapitalGains(csv);
383
384
  const totalGains = gains.reduce((s, g) => s + g.capital_gains_eur, 0);
@@ -425,7 +426,7 @@ export function registerLightyearTools(server, api) {
425
426
  fee_account: z.number().optional().describe("Fee expense account (default: fees included in investment cost)"),
426
427
  skip_tickers: z.string().optional().describe("Comma-separated tickers to skip (default: BRICEKSP)"),
427
428
  dry_run: z.boolean().optional().describe("Preview without creating entries (default true)"),
428
- }, batch, async ({ file_path, capital_gains_file, investment_account, investment_dimension_id, broker_account, broker_dimension_id, gain_loss_account, loss_account, fee_account, skip_tickers, dry_run }) => {
429
+ }, { ...batch, title: "Book Lightyear Trades" }, async ({ file_path, capital_gains_file, investment_account, investment_dimension_id, broker_account, broker_dimension_id, gain_loss_account, loss_account, fee_account, skip_tickers, dry_run }) => {
429
430
  const isDryRun = dry_run !== false;
430
431
  const skipSet = new Set((skip_tickers ?? CASH_FUND_TICKER).split(",").map(t => t.trim()));
431
432
  // Validate accounts exist and are active
@@ -481,7 +482,10 @@ export function registerLightyearTools(server, api) {
481
482
  const duplicates = trades.filter(t => existingRefs.has(t.reference));
482
483
  const results = [];
483
484
  const warnings = [...extraction.warnings, ...gainsWarnings];
484
- for (const trade of newTrades) {
485
+ const totalNewTrades = newTrades.length;
486
+ for (let tradeIdx = 0; tradeIdx < newTrades.length; tradeIdx++) {
487
+ const trade = newTrades[tradeIdx];
488
+ await reportProgress(tradeIdx, totalNewTrades);
485
489
  // Skip unmatched FX trades
486
490
  if (trade.ccy !== "EUR" && trade.eur_amount === 0) {
487
491
  results.push({
@@ -657,11 +661,7 @@ export function registerLightyearTools(server, api) {
657
661
  }],
658
662
  };
659
663
  });
660
- server.tool("book_lightyear_distributions", "Create journal entries for Lightyear dividend/interest distributions. " +
661
- "Checks for duplicates using reference IDs. " +
662
- "Books: Debit broker account (net received), Credit income account. " +
663
- "Income = gross (net + tax + fee). " +
664
- "Withheld tax (tax_amount) booked to tax_account. Platform fee booked to fee_account (default 8610).", {
664
+ server.tool("book_lightyear_distributions", "Create journal entries for Lightyear dividend and interest distributions, including withheld tax. DRY RUN by default.", {
665
665
  file_path: z.string().describe("Absolute path to Lightyear AccountStatement CSV file"),
666
666
  broker_account: z.number().describe("Broker cash account (e.g. 1120 Lightyear konto)"),
667
667
  broker_dimension_id: z.number().optional().describe("Dimension ID for broker account (accounts_dimensions_id)"),
@@ -669,7 +669,7 @@ export function registerLightyearTools(server, api) {
669
669
  tax_account: z.number().optional().describe("Withheld tax receivable/expense account (for tax_amount from CSV)"),
670
670
  fee_account: z.number().optional().describe("Platform fee expense account (default 8610 Muud finantskulud)"),
671
671
  dry_run: z.boolean().optional().describe("Preview without creating entries (default true)"),
672
- }, batch, async ({ file_path, broker_account, broker_dimension_id, income_account, tax_account, fee_account: fee_account_param, dry_run }) => {
672
+ }, { ...batch, title: "Book Lightyear Distributions" }, async ({ file_path, broker_account, broker_dimension_id, income_account, tax_account, fee_account: fee_account_param, dry_run }) => {
673
673
  const isDryRun = dry_run !== false;
674
674
  const fee_account = fee_account_param ?? 8610;
675
675
  // Validate accounts exist and are active
@@ -785,11 +785,9 @@ export function registerLightyearTools(server, api) {
785
785
  }],
786
786
  };
787
787
  });
788
- server.tool("lightyear_portfolio_summary", "Compute current portfolio holdings and cost basis from Lightyear account statement. " +
789
- "Uses weighted average cost method. Properly reduces cost basis on sells. " +
790
- "Useful for verifying investment account balance.", {
788
+ server.tool("lightyear_portfolio_summary", "Compute current holdings and cost basis from a Lightyear account statement. Useful for verifying investment account balance.", {
791
789
  file_path: z.string().describe("Absolute path to Lightyear AccountStatement CSV file"),
792
- }, readOnly, async ({ file_path }) => {
790
+ }, { ...readOnly, title: "Lightyear Portfolio Summary" }, async ({ file_path }) => {
793
791
  const csv = await readCsvFile(file_path);
794
792
  const rows = parseAccountStatement(csv);
795
793
  const { trades, warnings: fxWarnings } = extractTrades(rows);
@@ -46,12 +46,9 @@ function extractPdfHints(text) {
46
46
  return result;
47
47
  }
48
48
  export function registerPdfWorkflowTools(server, api) {
49
- server.tool("extract_pdf_invoice", "Extract text and machine-readable identifiers from a PDF invoice. " +
50
- "Returns raw text + detected IBAN, registry code, VAT number, reference number. " +
51
- "YOU must read the raw_text and extract: supplier name, invoice number, dates, " +
52
- "amounts (net, VAT, gross), and line items. Then call validate_invoice_data to verify.", {
49
+ server.tool("extract_pdf_invoice", "Extract text and key identifiers from a supplier invoice PDF. Returns raw text + detected IBAN, registry code, VAT number, reference number. Read raw_text to extract all invoice fields, then call validate_invoice_data.", {
53
50
  file_path: z.string().describe("Absolute path to the PDF file"),
54
- }, readOnly, async ({ file_path }) => {
51
+ }, { ...readOnly, title: "Extract Supplier Invoice PDF" }, async ({ file_path }) => {
55
52
  const resolved = await validatePdfPath(file_path);
56
53
  const buffer = await readFile(resolved);
57
54
  const pdfData = await pdf(buffer);
@@ -79,7 +76,7 @@ export function registerPdfWorkflowTools(server, api) {
79
76
  items: z.string().describe("JSON array of items with at least {total_net_price, vat_rate_dropdown?} each"),
80
77
  invoice_date: z.string().optional().describe("Invoice date (YYYY-MM-DD)"),
81
78
  due_date: z.string().optional().describe("Due date (YYYY-MM-DD)"),
82
- }, readOnly, async ({ total_net, total_vat, total_gross, items, invoice_date, due_date }) => {
79
+ }, { ...readOnly, title: "Validate Invoice Data" }, async ({ total_net, total_vat, total_gross, items, invoice_date, due_date }) => {
83
80
  const errors = [];
84
81
  const warnings = [];
85
82
  const parsed = safeJsonParse(items, "items");
@@ -187,9 +184,7 @@ export function registerPdfWorkflowTools(server, api) {
187
184
  }],
188
185
  };
189
186
  });
190
- server.tool("resolve_supplier", "Find or create a supplier in e-arveldaja. First searches by registry code, " +
191
- "then VAT number, then name (fuzzy). If not found, optionally creates a new client. " +
192
- "Also looks up business registry (äriregister) data if available.", {
187
+ server.tool("resolve_supplier", "Match a supplier to an existing client by registry code, VAT number, or name (fuzzy). Optionally creates a new client. Looks up Estonian business registry data.", {
193
188
  name: z.string().optional().describe("Supplier name from invoice"),
194
189
  reg_code: z.string().optional().describe("Registry code (registrikood)"),
195
190
  vat_no: z.string().optional().describe("VAT number (KMKR)"),
@@ -197,7 +192,7 @@ export function registerPdfWorkflowTools(server, api) {
197
192
  auto_create: z.boolean().optional().describe("Create client if not found (default false)"),
198
193
  country: z.string().optional().describe("Country code for auto-create (default EST)"),
199
194
  is_physical_entity: z.boolean().optional().describe("Natural person (default false = legal entity)"),
200
- }, create, async ({ name, reg_code, vat_no, iban, auto_create, country, is_physical_entity }) => {
195
+ }, { ...create, title: "Find or Create Supplier" }, async ({ name, reg_code, vat_no, iban, auto_create, country, is_physical_entity }) => {
201
196
  // 1. Search by registry code
202
197
  if (reg_code) {
203
198
  const byCode = await api.clients.findByCode(reg_code);
@@ -308,12 +303,11 @@ export function registerPdfWorkflowTools(server, api) {
308
303
  }],
309
304
  };
310
305
  });
311
- server.tool("suggest_booking", "Find similar past purchase invoices from the same supplier to suggest " +
312
- "how to book a new invoice (which accounts, articles, etc).", {
306
+ server.tool("suggest_booking", "Suggest purchase articles and accounts for a new invoice based on similar confirmed invoices from the same supplier.", {
313
307
  clients_id: z.number().describe("Supplier client ID"),
314
308
  description: z.string().optional().describe("Invoice item description to match"),
315
309
  limit: z.number().optional().describe("Max past invoices to return (default 3)"),
316
- }, readOnly, async ({ clients_id, description, limit }) => {
310
+ }, { ...readOnly, title: "Suggest Purchase Booking" }, async ({ clients_id, description, limit }) => {
317
311
  const maxResults = limit ?? 3;
318
312
  const allInvoices = await api.purchaseInvoices.listAll();
319
313
  // Filter by supplier
@@ -364,9 +358,7 @@ export function registerPdfWorkflowTools(server, api) {
364
358
  }],
365
359
  };
366
360
  });
367
- server.tool("create_purchase_invoice_from_pdf", "Full workflow: create a purchase invoice from extracted PDF data. " +
368
- "Resolves supplier, suggests booking, creates the invoice as DRAFT. " +
369
- "Pass EXACT vat_price and gross_price from the original invoice for payment matching.", {
361
+ server.tool("create_purchase_invoice_from_pdf", "Create a draft purchase invoice from extracted and validated PDF data. Pass EXACT vat_price and gross_price from the original invoice for payment matching.", {
370
362
  supplier_client_id: z.number().describe("Supplier client ID (from resolve_supplier)"),
371
363
  invoice_number: z.string().describe("Invoice number"),
372
364
  invoice_date: z.string().describe("Invoice date (YYYY-MM-DD)"),
@@ -379,7 +371,7 @@ export function registerPdfWorkflowTools(server, api) {
379
371
  notes: z.string().optional().describe("Notes (e.g. PDF filename)"),
380
372
  ref_number: z.string().optional().describe("Reference number"),
381
373
  bank_account_no: z.string().optional().describe("Supplier bank account"),
382
- }, create, async (params) => {
374
+ }, { ...create, title: "Create Purchase Invoice from PDF" }, async (params) => {
383
375
  const supplier = await api.clients.get(params.supplier_client_id);
384
376
  const isVatReg = await isCompanyVatRegistered(api);
385
377
  const purchaseArticles = await getPurchaseArticlesWithVat(api);
@@ -413,7 +405,7 @@ export function registerPdfWorkflowTools(server, api) {
413
405
  server.tool("upload_invoice_document", "Upload a PDF document to an existing purchase invoice", {
414
406
  invoice_id: z.number().describe("Purchase invoice ID"),
415
407
  file_path: z.string().describe("Absolute path to the PDF file"),
416
- }, mutate, async ({ invoice_id, file_path }) => {
408
+ }, { ...mutate, title: "Upload Purchase Invoice PDF" }, async ({ invoice_id, file_path }) => {
417
409
  const resolved = await validatePdfPath(file_path);
418
410
  const buffer = await readFile(resolved);
419
411
  const base64 = buffer.toString("base64");
@@ -9,7 +9,7 @@ export function registerRecurringInvoiceTools(server, api) {
9
9
  target_journal_date: z.string().describe("New turnover date (YYYY-MM-DD)"),
10
10
  invoice_ids: z.string().optional().describe("Comma-separated source invoice IDs to copy (default: all confirmed from source month)"),
11
11
  auto_confirm: z.boolean().optional().describe("Confirm created invoices (default false)"),
12
- }, batch, async ({ source_month, target_date, target_journal_date, invoice_ids, auto_confirm }) => {
12
+ }, { ...batch, title: "Create Recurring Sale Invoices" }, async ({ source_month, target_date, target_journal_date, invoice_ids, auto_confirm }) => {
13
13
  // Get source invoices
14
14
  const allSales = await api.saleInvoices.listAll();
15
15
  const sourceFrom = `${source_month}-01`;
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { readFile } from "fs/promises";
3
3
  import { validateFilePath } from "../file-validation.js";
4
4
  import { batch } from "../annotations.js";
5
+ import { reportProgress } from "../progress.js";
5
6
  const EXPECTED_HEADERS = [
6
7
  "ID", "Status", "Direction", "Created on", "Finished on",
7
8
  "Source fee amount", "Source fee currency", "Target fee amount", "Target fee currency",
@@ -89,7 +90,7 @@ export function registerWiseImportTools(server, api) {
89
90
  execute: z.boolean().optional().describe("Actually create transactions (default false = dry run)"),
90
91
  date_from: z.string().optional().describe("Only import transactions from this date (YYYY-MM-DD)"),
91
92
  date_to: z.string().optional().describe("Only import transactions up to this date (YYYY-MM-DD)"),
92
- }, batch, async ({ file_path, accounts_dimensions_id, fee_account_relation_id, execute, date_from, date_to }) => {
93
+ }, { ...batch, title: "Import Wise Transactions" }, async ({ file_path, accounts_dimensions_id, fee_account_relation_id, execute, date_from, date_to }) => {
93
94
  const resolved = await validateFilePath(file_path, [".csv"], 10 * 1024 * 1024);
94
95
  const csv = await readFile(resolved, "utf-8");
95
96
  const rows = parseWiseCSV(csv);
@@ -126,7 +127,10 @@ export function registerWiseImportTools(server, api) {
126
127
  .map(tx => tx.description.split(" ")[0]));
127
128
  const created = [];
128
129
  const skipped = [];
129
- for (const row of eligible) {
130
+ const totalEligible = eligible.length;
131
+ for (let i = 0; i < eligible.length; i++) {
132
+ const row = eligible[i];
133
+ await reportProgress(i, totalEligible);
130
134
  const date = wiseDate(row.finishedOn || row.createdOn);
131
135
  const type = "C"; // e-arveldaja uses type C for all bank transactions
132
136
  const amount = row.sourceAmount;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "e-arveldaja-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "MCP server for Estonian e-arveldaja (e-Financials) API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",