e-arveldaja-mcp 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # e-arveldaja MCP Server
2
2
 
3
- MCP (Model Context Protocol) server for the Estonian e-arveldaja (RIK e-Financials) REST API. Works with any MCP-compatible AI assistant — Claude Code, Cursor, Windsurf, Cline, and others.
3
+ [![npm](https://img.shields.io/npm/v/e-arveldaja-mcp)](https://www.npmjs.com/package/e-arveldaja-mcp)
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.
4
6
 
5
7
  ## Disclaimer
6
8
 
@@ -19,7 +21,7 @@ By using this software you acknowledge that:
19
21
  2. Go to **Seadistused** → **Üldised seadistused** → **Lisa uus juurdepääsuluba** (Settings → General settings → Add new access token)
20
22
  3. Enter any name for the token
21
23
  4. Find your public IP address (e.g. at [whatismyipaddress.com](https://whatismyipaddress.com/)) and enter it in the allowed IP field. Multiple IPs can be separated by `;`
22
- 5. Save — download the `apikey.txt` file and place it next to the project directory (i.e. in the parent folder)
24
+ 5. Save — download the `apikey.txt` file and place it in the working directory where you run your AI assistant
23
25
 
24
26
  If you don't have a static IP address, you will need to update the allowed IP in e-arveldaja settings whenever your IP changes.
25
27
 
@@ -46,11 +48,9 @@ npm run build # tsc -> dist/
46
48
 
47
49
  This is a standard MCP server using stdio transport. Most AI assistants can set this up themselves — just ask:
48
50
 
49
- > "Add the e-arveldaja MCP server from /path/to/e-arveldaja-mcp to my MCP configuration"
50
-
51
- The assistant will read the config format it needs and add the server entry. You can also ask it to install the workflows:
51
+ > "Add the e-arveldaja-mcp npm package as an MCP server to my configuration, using npx"
52
52
 
53
- > "Copy the workflow files from /path/to/e-arveldaja-mcp/workflows/ into your commands so I can use them"
53
+ The assistant will add `{"command": "npx", "args": ["-y", "e-arveldaja-mcp"]}` to its MCP config. No cloning or paths needed.
54
54
 
55
55
  If you prefer to configure manually:
56
56
 
@@ -10,6 +10,7 @@ export declare class TransactionsApi extends BaseResource<Transaction> {
10
10
  * rejects confirmation with "buyer or supplier is missing".
11
11
  */
12
12
  confirm(id: number, distributions?: TransactionDistribution[]): Promise<ApiResponse>;
13
+ invalidate(id: number): Promise<ApiResponse>;
13
14
  getDocument(id: number): Promise<ApiFile>;
14
15
  uploadDocument(id: number, name: string, contents: string): Promise<ApiResponse>;
15
16
  deleteDocument(id: number): Promise<ApiResponse>;
@@ -36,6 +36,10 @@ export class TransactionsApi extends BaseResource {
36
36
  }
37
37
  return this.client.patch(`/transactions/${id}/register`, body);
38
38
  }
39
+ async invalidate(id) {
40
+ this.invalidateCache();
41
+ return this.client.patch(`/transactions/${id}/invalidate`, {});
42
+ }
39
43
  async getDocument(id) {
40
44
  return this.client.get(`/transactions/${id}/document_user`);
41
45
  }
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ import { registerRecurringInvoiceTools } from "./tools/recurring-invoices.js";
23
23
  import { registerEstonianTaxTools } from "./tools/estonian-tax.js";
24
24
  import { registerDocumentAuditTools } from "./tools/document-audit.js";
25
25
  import { registerLightyearTools } from "./tools/lightyear-investments.js";
26
+ import { registerWiseImportTools } from "./tools/wise-import.js";
26
27
  import { registerResources } from "./resources/static-resources.js";
27
28
  function buildApiContext(httpClient) {
28
29
  return {
@@ -203,6 +204,7 @@ async function main() {
203
204
  registerEstonianTaxTools(server, api);
204
205
  registerDocumentAuditTools(server, api);
205
206
  registerLightyearTools(server, api);
207
+ registerWiseImportTools(server, api);
206
208
  // Register resources
207
209
  registerResources(server, api);
208
210
  // Start server
@@ -255,6 +255,10 @@ export function registerCrudTools(server, api) {
255
255
  const result = await api.transactions.confirm(id, dist);
256
256
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
257
257
  });
258
+ server.tool("invalidate_transaction", "Invalidate (unconfirm) a confirmed transaction. Returns it to unconfirmed status for editing or deletion.", idParam.shape, async ({ id }) => {
259
+ const result = await api.transactions.invalidate(id);
260
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
261
+ });
258
262
  server.tool("delete_transaction", "Delete a transaction", idParam.shape, async ({ id }) => {
259
263
  const result = await api.transactions.delete(id);
260
264
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiContext } from "./crud-tools.js";
3
+ export declare function registerWiseImportTools(server: McpServer, api: ApiContext): void;
@@ -0,0 +1,264 @@
1
+ import { z } from "zod";
2
+ import { readFile } from "fs/promises";
3
+ import { validateFilePath } from "../file-validation.js";
4
+ const EXPECTED_HEADERS = [
5
+ "ID", "Status", "Direction", "Created on", "Finished on",
6
+ "Source fee amount", "Source fee currency", "Target fee amount", "Target fee currency",
7
+ "Source name", "Source amount (after fees)", "Source currency",
8
+ "Target name", "Target amount (after fees)", "Target currency",
9
+ "Exchange rate", "Reference", "Batch", "Created by", "Category", "Note",
10
+ ];
11
+ function parseCSVLine(line) {
12
+ const fields = [];
13
+ let current = "";
14
+ let inQuotes = false;
15
+ for (let i = 0; i < line.length; i++) {
16
+ const ch = line[i];
17
+ if (ch === '"') {
18
+ if (inQuotes && line[i + 1] === '"') {
19
+ current += '"';
20
+ i++;
21
+ }
22
+ else {
23
+ inQuotes = !inQuotes;
24
+ }
25
+ }
26
+ else if (ch === "," && !inQuotes) {
27
+ fields.push(current);
28
+ current = "";
29
+ }
30
+ else {
31
+ current += ch;
32
+ }
33
+ }
34
+ fields.push(current);
35
+ return fields;
36
+ }
37
+ function parseWiseCSV(csv) {
38
+ const lines = csv.trim().split("\n").filter(l => l.trim());
39
+ if (lines.length < 2)
40
+ throw new Error("CSV has no data rows");
41
+ const headers = parseCSVLine(lines[0]);
42
+ // Validate key headers exist
43
+ for (const expected of ["ID", "Status", "Direction", "Source amount (after fees)"]) {
44
+ if (!headers.includes(expected)) {
45
+ throw new Error(`Missing expected header "${expected}". Found: ${headers.slice(0, 10).join(", ")}`);
46
+ }
47
+ }
48
+ const idx = (name) => headers.indexOf(name);
49
+ const rows = [];
50
+ for (let i = 1; i < lines.length; i++) {
51
+ const fields = parseCSVLine(lines[i]);
52
+ if (fields.length < headers.length - 2)
53
+ continue; // allow slightly short rows
54
+ rows.push({
55
+ id: fields[idx("ID")] ?? "",
56
+ status: fields[idx("Status")] ?? "",
57
+ direction: fields[idx("Direction")] ?? "",
58
+ createdOn: fields[idx("Created on")] ?? "",
59
+ finishedOn: fields[idx("Finished on")] ?? "",
60
+ sourceFeeAmount: parseFloat(fields[idx("Source fee amount")] || "0") || 0,
61
+ sourceFeeCurrency: fields[idx("Source fee currency")] ?? "EUR",
62
+ sourceAmount: parseFloat(fields[idx("Source amount (after fees)")] || "0") || 0,
63
+ sourceCurrency: fields[idx("Source currency")] ?? "EUR",
64
+ targetName: fields[idx("Target name")] ?? "",
65
+ targetAmount: parseFloat(fields[idx("Target amount (after fees)")] || "0") || 0,
66
+ targetCurrency: fields[idx("Target currency")] ?? "EUR",
67
+ exchangeRate: parseFloat(fields[idx("Exchange rate")] || "1") || 1,
68
+ reference: fields[idx("Reference")] ?? "",
69
+ category: fields[idx("Category")] ?? "",
70
+ note: fields[idx("Note")] ?? "",
71
+ });
72
+ }
73
+ return rows;
74
+ }
75
+ function wiseDate(dateStr) {
76
+ // "2026-01-19 17:59:56" → "2026-01-19"
77
+ return dateStr.split(" ")[0] ?? dateStr;
78
+ }
79
+ export function registerWiseImportTools(server, api) {
80
+ server.tool("import_wise_transactions", "Parse a Wise transaction history CSV and create bank transactions in e-arveldaja. " +
81
+ "Skips REFUNDED, NEUTRAL, and zero-amount entries. " +
82
+ "All transactions use type C (e-arveldaja convention). " +
83
+ "Wise fees are created as separate transactions (for correct VAT/expense treatment). " +
84
+ "DRY RUN by default — set execute=true to actually create transactions.", {
85
+ file_path: z.string().describe("Absolute path to Wise transaction-history.csv"),
86
+ accounts_dimensions_id: z.number().describe("Bank account dimension ID for the Wise account in e-arveldaja"),
87
+ fee_account_relation_id: z.number().optional().describe("Relation ID for fee account distribution (default 861012637379 = 8610 Muud finantskulud)"),
88
+ execute: z.boolean().optional().describe("Actually create transactions (default false = dry run)"),
89
+ date_from: z.string().optional().describe("Only import transactions from this date (YYYY-MM-DD)"),
90
+ date_to: z.string().optional().describe("Only import transactions up to this date (YYYY-MM-DD)"),
91
+ }, async ({ file_path, accounts_dimensions_id, fee_account_relation_id, execute, date_from, date_to }) => {
92
+ const resolved = await validateFilePath(file_path, [".csv"], 10 * 1024 * 1024);
93
+ const csv = await readFile(resolved, "utf-8");
94
+ const rows = parseWiseCSV(csv);
95
+ const dryRun = execute !== true;
96
+ const feeRelationId = fee_account_relation_id ?? 861012637379; // 8610 Muud finantskulud
97
+ // Find Wise client for fee transactions
98
+ let wiseClientId;
99
+ if (!dryRun) {
100
+ const allClients = await api.clients.listAll();
101
+ const wiseClient = allClients.find(c => c.name?.toUpperCase() === "WISE" || c.name?.toUpperCase() === "TRANSFERWISE");
102
+ wiseClientId = wiseClient?.id;
103
+ }
104
+ // Filter rows
105
+ const eligible = rows.filter(r => {
106
+ if (r.status !== "COMPLETED")
107
+ return false;
108
+ if (r.direction === "NEUTRAL")
109
+ return false;
110
+ if (r.sourceAmount === 0 && r.targetAmount === 0)
111
+ return false;
112
+ const date = wiseDate(r.finishedOn || r.createdOn);
113
+ if (date_from && date < date_from)
114
+ return false;
115
+ if (date_to && date > date_to)
116
+ return false;
117
+ return true;
118
+ });
119
+ // Get existing transactions for duplicate detection
120
+ const existingTx = await api.transactions.listAll();
121
+ const existingDescs = new Set(existingTx.map(tx => `${tx.date}|${tx.amount}|${tx.description ?? ""}`));
122
+ // Also check by Wise ID in description
123
+ const existingByWiseId = new Set(existingTx
124
+ .filter(tx => tx.description?.startsWith("WISE:"))
125
+ .map(tx => tx.description.split(" ")[0]));
126
+ const created = [];
127
+ const skipped = [];
128
+ for (const row of eligible) {
129
+ const date = wiseDate(row.finishedOn || row.createdOn);
130
+ const type = "C"; // e-arveldaja uses type C for all bank transactions
131
+ const amount = row.sourceAmount;
132
+ const fee = row.sourceFeeAmount;
133
+ const wiseIdTag = `WISE:${row.id}`;
134
+ // Build description
135
+ let desc = wiseIdTag;
136
+ if (row.targetName)
137
+ desc += ` ${row.targetName}`;
138
+ if (row.category && row.category !== "General")
139
+ desc += ` (${row.category})`;
140
+ if (row.targetCurrency !== "EUR" && row.targetCurrency !== row.sourceCurrency) {
141
+ desc += ` [${row.targetAmount} ${row.targetCurrency} @ ${row.exchangeRate}]`;
142
+ }
143
+ // Duplicate check
144
+ if (existingByWiseId.has(wiseIdTag)) {
145
+ skipped.push({ wise_id: row.id, reason: "Already imported (Wise ID match)" });
146
+ continue;
147
+ }
148
+ // Create the main transaction (net amount, without fee)
149
+ if (dryRun) {
150
+ created.push({
151
+ wise_id: row.id,
152
+ date,
153
+ type,
154
+ amount,
155
+ description: desc,
156
+ status: "would_create",
157
+ });
158
+ }
159
+ else {
160
+ try {
161
+ const result = await api.transactions.create({
162
+ accounts_dimensions_id,
163
+ type,
164
+ amount,
165
+ cl_currencies_id: "EUR",
166
+ date,
167
+ description: desc,
168
+ bank_account_name: row.targetName || undefined,
169
+ ref_number: row.reference || undefined,
170
+ });
171
+ created.push({
172
+ wise_id: row.id,
173
+ date,
174
+ type,
175
+ amount,
176
+ description: desc,
177
+ status: "created",
178
+ api_id: result.created_object_id,
179
+ });
180
+ }
181
+ catch (err) {
182
+ skipped.push({ wise_id: row.id, reason: err.message });
183
+ }
184
+ }
185
+ // Create separate fee transaction if fee > 0
186
+ if (fee > 0) {
187
+ const feeDesc = `WISE:FEE:${row.id} Wise teenustasu`;
188
+ if (dryRun) {
189
+ created.push({
190
+ wise_id: `FEE:${row.id}`,
191
+ date,
192
+ type,
193
+ amount: fee,
194
+ description: feeDesc,
195
+ status: "would_create",
196
+ });
197
+ }
198
+ else {
199
+ try {
200
+ const feeResult = await api.transactions.create({
201
+ accounts_dimensions_id,
202
+ type,
203
+ amount: fee,
204
+ cl_currencies_id: "EUR",
205
+ date,
206
+ description: feeDesc,
207
+ bank_account_name: "Wise",
208
+ clients_id: wiseClientId,
209
+ });
210
+ const feeId = feeResult.created_object_id;
211
+ // Auto-confirm fee to expense account
212
+ if (feeId && wiseClientId) {
213
+ try {
214
+ await api.transactions.confirm(feeId, [
215
+ { related_table: "accounts", related_id: feeRelationId, amount: fee },
216
+ ]);
217
+ created.push({
218
+ wise_id: `FEE:${row.id}`,
219
+ date, type, amount: fee, description: feeDesc,
220
+ status: "created_and_confirmed",
221
+ api_id: feeId,
222
+ });
223
+ }
224
+ catch (confErr) {
225
+ created.push({
226
+ wise_id: `FEE:${row.id}`,
227
+ date, type, amount: fee, description: feeDesc,
228
+ status: "created (confirm failed: " + confErr.message + ")",
229
+ api_id: feeId,
230
+ });
231
+ }
232
+ }
233
+ else {
234
+ created.push({
235
+ wise_id: `FEE:${row.id}`,
236
+ date, type, amount: fee, description: feeDesc,
237
+ status: "created (needs manual confirm)",
238
+ api_id: feeId,
239
+ });
240
+ }
241
+ }
242
+ catch (err) {
243
+ skipped.push({ wise_id: `FEE:${row.id}`, reason: err.message });
244
+ }
245
+ }
246
+ }
247
+ }
248
+ return {
249
+ content: [{
250
+ type: "text",
251
+ text: JSON.stringify({
252
+ mode: dryRun ? "DRY_RUN" : "EXECUTED",
253
+ total_csv_rows: rows.length,
254
+ eligible: eligible.length,
255
+ filtered_out: rows.length - eligible.length,
256
+ created: created.length,
257
+ skipped: skipped.length,
258
+ results: created,
259
+ skipped_details: skipped,
260
+ }, null, 2),
261
+ }],
262
+ };
263
+ });
264
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "e-arveldaja-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for Estonian e-arveldaja (e-Financials) API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",