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 +2 -2
- package/README.md +49 -53
- package/dist/api/base-resource.js +4 -0
- package/dist/index.js +26 -4
- package/dist/progress.d.ts +13 -0
- package/dist/progress.js +24 -0
- package/dist/tools/account-balance.js +4 -9
- package/dist/tools/aging-analysis.js +2 -2
- package/dist/tools/bank-reconciliation.js +8 -5
- package/dist/tools/crud-tools.js +59 -63
- package/dist/tools/document-audit.js +3 -3
- package/dist/tools/estonian-tax.js +4 -9
- package/dist/tools/financial-statements.js +4 -4
- package/dist/tools/lightyear-investments.js +12 -14
- package/dist/tools/pdf-workflow.js +10 -18
- package/dist/tools/recurring-invoices.js +1 -1
- package/dist/tools/wise-import.js +6 -2
- package/package.json +1 -1
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
|
-
|
|
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(); //
|
|
168
|
+
const { tools } = await client.listTools(); // 85 tools
|
|
169
169
|
```
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/e-arveldaja-mcp)
|
|
4
4
|
|
|
5
|
-
MCP
|
|
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
|
-
###
|
|
34
|
+
### 1. Add the MCP server
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Most AI assistants can set this up for you — just ask:
|
|
37
37
|
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
86
|
-
| **Gemini CLI** | `~/.gemini/settings.json`
|
|
87
|
-
| **Google Antigravity** | MCP Store UI → Manage MCP Servers →
|
|
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
|
-
|
|
72
|
+
</details>
|
|
93
73
|
|
|
94
|
-
|
|
74
|
+
### 2. Place your API key
|
|
95
75
|
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
<details>
|
|
81
|
+
<summary>Alternative: environment variables</summary>
|
|
106
82
|
|
|
107
|
-
|
|
83
|
+
```bash
|
|
84
|
+
export EARVELDAJA_API_KEY_ID=...
|
|
85
|
+
export EARVELDAJA_API_PUBLIC_VALUE=...
|
|
86
|
+
export EARVELDAJA_API_PASSWORD=...
|
|
87
|
+
```
|
|
108
88
|
|
|
109
|
-
|
|
89
|
+
</details>
|
|
110
90
|
|
|
111
|
-
|
|
91
|
+
<details>
|
|
92
|
+
<summary>Building from source</summary>
|
|
112
93
|
|
|
113
94
|
```bash
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
#
|
|
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
|
-
|
|
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.
|
|
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
|
|
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>;
|
package/dist/progress.js
ADDED
|
@@ -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
|
|
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
|
|
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", "
|
|
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
|
-
|
|
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}"` });
|
package/dist/tools/crud-tools.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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", "
|
|
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
|
|
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
|
|
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", "
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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
|
-
|
|
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;
|