financy 0.3.0 → 0.4.1
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 +167 -1
- package/dist/bin.js +219 -7
- package/dist/{chunk-VE5WZRGU.js → chunk-FKAHEAV6.js} +239 -5
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +20 -5
- package/dist/server-RVYFSTUB.js +459 -0
- package/package.json +2 -2
- package/skills/financy-setup/SKILL.md +33 -4
- package/dist/server-RM4BAVES.js +0 -207
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CliError,
|
|
4
|
+
EXIT,
|
|
5
|
+
VERSION,
|
|
6
|
+
createDraft,
|
|
7
|
+
fetchAccount,
|
|
8
|
+
fetchAccounts,
|
|
9
|
+
fetchBranches,
|
|
10
|
+
fetchCategories,
|
|
11
|
+
fetchConnection,
|
|
12
|
+
fetchConnections,
|
|
13
|
+
fetchIntegration,
|
|
14
|
+
fetchIntegrations,
|
|
15
|
+
fetchInvoicingCustomer,
|
|
16
|
+
fetchInvoicingCustomers,
|
|
17
|
+
fetchInvoicingDocument,
|
|
18
|
+
fetchInvoicingDocuments,
|
|
19
|
+
fetchProposal,
|
|
20
|
+
fetchProposals,
|
|
21
|
+
fetchProviders,
|
|
22
|
+
fetchReceivables,
|
|
23
|
+
fetchRefresh,
|
|
24
|
+
fetchStatus,
|
|
25
|
+
fetchTransaction,
|
|
26
|
+
fetchTransactions,
|
|
27
|
+
loadConfig,
|
|
28
|
+
notConfigured
|
|
29
|
+
} from "./chunk-FKAHEAV6.js";
|
|
30
|
+
|
|
31
|
+
// src/mcp/server.ts
|
|
32
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
33
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
34
|
+
import {
|
|
35
|
+
ListToolsRequestSchema,
|
|
36
|
+
CallToolRequestSchema
|
|
37
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
38
|
+
|
|
39
|
+
// src/mcp/tools.ts
|
|
40
|
+
var str = (v) => v == null ? void 0 : String(v);
|
|
41
|
+
var bool = (v) => v === true;
|
|
42
|
+
function requireId(args) {
|
|
43
|
+
const id = args.id;
|
|
44
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
45
|
+
throw new CliError(EXIT.USAGE, "MISSING_ARGUMENT", "the `id` argument is required");
|
|
46
|
+
}
|
|
47
|
+
return id;
|
|
48
|
+
}
|
|
49
|
+
var PAGING = {
|
|
50
|
+
limit: { type: "number", description: "page size (defaults to the API default)" },
|
|
51
|
+
cursor: { type: "string", description: "opaque nextPage cursor from a prior call" },
|
|
52
|
+
all: { type: "boolean", description: "auto-paginate to the end (ignores cursor/limit paging)" }
|
|
53
|
+
};
|
|
54
|
+
var NO_ARGS = { type: "object", properties: {} };
|
|
55
|
+
var idSchema = (noun) => ({
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: { id: { type: "string", description: `the ${noun} id` } },
|
|
58
|
+
required: ["id"]
|
|
59
|
+
});
|
|
60
|
+
var listArgs = (a) => ({
|
|
61
|
+
limit: str(a.limit),
|
|
62
|
+
cursor: str(a.cursor),
|
|
63
|
+
all: bool(a.all)
|
|
64
|
+
});
|
|
65
|
+
var TOOLS = [
|
|
66
|
+
{
|
|
67
|
+
name: "list_connections",
|
|
68
|
+
description: "List the user's bank/card connections and their fetch state (status, data-through date, consent expiry, per-type counts). Start here to see what is connected. Supports limit/cursor paging and all.",
|
|
69
|
+
inputSchema: { type: "object", properties: { ...PAGING } },
|
|
70
|
+
run: (a, ctx) => fetchConnections(ctx.config, ctx.now, listArgs(a))
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "get_connection",
|
|
74
|
+
description: "Read a single connection by id (from list_connections). Errors CONNECTION_NOT_FOUND if it is not in the user's scope.",
|
|
75
|
+
inputSchema: idSchema("connection"),
|
|
76
|
+
run: (a, ctx) => fetchConnection(ctx.config, ctx.now, requireId(a))
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "list_accounts",
|
|
80
|
+
description: "List accounts with balances (securities embedded). Filter by type (CHECKING|CARD|LOAN|SAVINGS|SECURITY) or connection. Supports limit/cursor/all.",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
type: { type: "string", description: "CHECKING|CARD|LOAN|SAVINGS|SECURITY" },
|
|
85
|
+
connection: { type: "string", description: "filter to one connection id" },
|
|
86
|
+
...PAGING
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
run: (a, ctx) => fetchAccounts(ctx.config, ctx.now, {
|
|
90
|
+
...listArgs(a),
|
|
91
|
+
type: str(a.type),
|
|
92
|
+
connection: str(a.connection)
|
|
93
|
+
})
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "get_account",
|
|
97
|
+
description: "Read a single account by id (from list_accounts). Errors ACCOUNT_NOT_FOUND if not in scope.",
|
|
98
|
+
inputSchema: idSchema("account"),
|
|
99
|
+
run: (a, ctx) => fetchAccount(ctx.config, ctx.now, requireId(a))
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "list_transactions",
|
|
103
|
+
description: "List transactions, most useful with filters: from/to (YYYY-MM-DD), account, connection, type (BANK|CARD). Page with limit/cursor or fetch everything with all. Hebrew merchant names are returned as-is. To then read one transaction, pass its `SK` (not its `id`) to get_transaction.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
from: { type: "string", description: "earliest transaction date (YYYY-MM-DD)" },
|
|
108
|
+
to: { type: "string", description: "latest transaction date (YYYY-MM-DD)" },
|
|
109
|
+
account: { type: "string", description: "filter to one account id" },
|
|
110
|
+
connection: { type: "string", description: "filter to one connection id" },
|
|
111
|
+
type: { type: "string", description: "BANK|CARD" },
|
|
112
|
+
...PAGING
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
run: (a, ctx) => fetchTransactions(ctx.config, ctx.now, {
|
|
116
|
+
...listArgs(a),
|
|
117
|
+
from: str(a.from),
|
|
118
|
+
to: str(a.to),
|
|
119
|
+
account: str(a.account),
|
|
120
|
+
connection: str(a.connection),
|
|
121
|
+
type: str(a.type)
|
|
122
|
+
})
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "get_transaction",
|
|
126
|
+
description: "Read a single transaction by its `SK` \u2014 the composite key from list_transactions, NOT the short `id` field, which this route does not accept. Errors TRANSACTION_NOT_FOUND if not in scope.",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
id: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: 'the transaction\'s `SK` from list_transactions, e.g. "TRANSACTION#TYPE#CHECKING#PROVIDER#leumi#RESOURCE<uuid>#<ulid>". The short `id` field of the same transaction will NOT resolve.'
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
required: ["id"]
|
|
136
|
+
},
|
|
137
|
+
run: (a, ctx) => fetchTransaction(ctx.config, ctx.now, requireId(a))
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "list_categories",
|
|
141
|
+
description: "The static transaction-category taxonomy (English + Hebrew). Use to interpret transaction category fields. No arguments.",
|
|
142
|
+
inputSchema: NO_ARGS,
|
|
143
|
+
run: async (_a, ctx) => ({ data: await fetchCategories(ctx.config, ctx.now) })
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "list_providers",
|
|
147
|
+
description: "Reference data: the list of banks/providers, to resolve providerId values. No arguments.",
|
|
148
|
+
inputSchema: NO_ARGS,
|
|
149
|
+
run: (_a, ctx) => fetchProviders(ctx.config, ctx.now)
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "list_bank_branches",
|
|
153
|
+
description: "Reference data: bank branches. No arguments (this route takes no paging).",
|
|
154
|
+
inputSchema: NO_ARGS,
|
|
155
|
+
run: (_a, ctx) => fetchBranches(ctx.config, ctx.now)
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "get_status",
|
|
159
|
+
description: "Per-connection freshness rollup: for each connection its status, data-through date, consent expiry, and account count, plus staleThresholdDays. Call this first to decide whether data is fresh enough before answering questions about balances or transactions.",
|
|
160
|
+
inputSchema: NO_ARGS,
|
|
161
|
+
run: async (_a, ctx) => {
|
|
162
|
+
const { data, staleThresholdDays } = await fetchStatus(ctx.config, ctx.now);
|
|
163
|
+
return { data, staleThresholdDays };
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "refresh_connections",
|
|
168
|
+
description: "Trigger an on-demand refresh of ALL connections (org-wide). This COSTS 20 credits \u2014 always confirm with the user before calling it. Data lands after a few minutes; poll get_status to watch progress. Returns the refresh outcome (accepted / already_running).",
|
|
169
|
+
inputSchema: NO_ARGS,
|
|
170
|
+
run: (_a, ctx) => fetchRefresh(ctx.config, ctx.now)
|
|
171
|
+
}
|
|
172
|
+
];
|
|
173
|
+
var BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
|
|
174
|
+
function errorEnvelope(error) {
|
|
175
|
+
const e = error instanceof CliError ? error : new CliError(EXIT.UNEXPECTED, "UNEXPECTED", String(error?.message ?? error));
|
|
176
|
+
return { error: { code: e.code, message: e.message, ...e.extra } };
|
|
177
|
+
}
|
|
178
|
+
async function dispatchTool(byName, name, args, io) {
|
|
179
|
+
try {
|
|
180
|
+
const tool = byName.get(name);
|
|
181
|
+
if (!tool) {
|
|
182
|
+
throw new CliError(EXIT.USAGE, "UNKNOWN_TOOL", `no tool named '${name}'`);
|
|
183
|
+
}
|
|
184
|
+
let config = io.config;
|
|
185
|
+
if (!config) {
|
|
186
|
+
const result = await loadConfig(io.env);
|
|
187
|
+
if (!result.ok) throw notConfigured(result);
|
|
188
|
+
config = result.config;
|
|
189
|
+
}
|
|
190
|
+
return await tool.run(args ?? {}, { config, now: io.now ?? /* @__PURE__ */ new Date() });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return errorEnvelope(error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/mcp/integration-tools.ts
|
|
197
|
+
var str2 = (v) => v == null ? void 0 : String(v);
|
|
198
|
+
var bool2 = (v) => v === true;
|
|
199
|
+
function required(args, key) {
|
|
200
|
+
const value = args[key];
|
|
201
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
202
|
+
throw new CliError(EXIT.USAGE, "MISSING_ARGUMENT", `the \`${key}\` argument is required`);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
function selected(args, keys) {
|
|
207
|
+
return Object.fromEntries(keys.filter((key) => args[key] !== void 0).map((key) => [key, args[key]]));
|
|
208
|
+
}
|
|
209
|
+
function paymentOf(args) {
|
|
210
|
+
return {
|
|
211
|
+
method: args.paymentMethod,
|
|
212
|
+
date: args.paymentDate,
|
|
213
|
+
...args.paymentReference === void 0 ? {} : { reference: args.paymentReference }
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
var AMOUNTS = "Amounts are integers in minor units of their currency (agorot for ILS).";
|
|
217
|
+
var DRAFT = "Create a document DRAFT. Nothing is issued by this call: the user must approve the draft in Financy at the returned approvalUrl, and only then is the document issued in their business system. Show the user the approvalUrl. " + AMOUNTS;
|
|
218
|
+
var PAGING_NOTE = "A page shorter than limit with a non-null nextPage means more may match: call again with that cursor, or pass all.";
|
|
219
|
+
var integrationId = { type: "string", description: "an integration id (intg_\u2026) from list_integrations" };
|
|
220
|
+
var documentId = { type: "string", description: "a document id (idoc_\u2026) from list_invoicing_documents" };
|
|
221
|
+
var counterpartyId = { type: "string", description: "a customer id (icus_\u2026) from list_invoicing_customers" };
|
|
222
|
+
var proposalId = { type: "string", description: "a proposal id (prop_\u2026) returned by a draft tool" };
|
|
223
|
+
var localDate = (description) => ({ type: "string", description });
|
|
224
|
+
var PAGING2 = {
|
|
225
|
+
limit: { type: "number", description: "page size (1\u2013200)" },
|
|
226
|
+
cursor: { type: "string", description: "opaque nextPage cursor from a prior call" },
|
|
227
|
+
all: { type: "boolean", description: "auto-paginate to the end (ignores cursor/limit paging)" }
|
|
228
|
+
};
|
|
229
|
+
var recipients = {
|
|
230
|
+
type: "array",
|
|
231
|
+
items: { type: "string" },
|
|
232
|
+
description: "up to three email recipients; absent or empty sends no email"
|
|
233
|
+
};
|
|
234
|
+
var paymentProperties = {
|
|
235
|
+
paymentMethod: { type: "string", enum: ["bank_transfer", "cash", "cheque", "card", "other"] },
|
|
236
|
+
paymentDate: localDate("payment date, YYYY-MM-DD"),
|
|
237
|
+
paymentReference: { type: "string", description: "e.g. a transfer reference" }
|
|
238
|
+
};
|
|
239
|
+
var invoiceProperties = {
|
|
240
|
+
integrationId,
|
|
241
|
+
counterpartyId,
|
|
242
|
+
lines: {
|
|
243
|
+
type: "array",
|
|
244
|
+
description: "1\u201320 lines",
|
|
245
|
+
items: {
|
|
246
|
+
type: "object",
|
|
247
|
+
properties: {
|
|
248
|
+
description: { type: "string" },
|
|
249
|
+
quantity: { type: "integer" },
|
|
250
|
+
unitPriceMinor: { type: "integer" }
|
|
251
|
+
},
|
|
252
|
+
required: ["description", "quantity", "unitPriceMinor"]
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
pricesIncludeVat: { type: "boolean" },
|
|
256
|
+
dueDate: localDate("due date, YYYY-MM-DD"),
|
|
257
|
+
recipients
|
|
258
|
+
};
|
|
259
|
+
var invoiceRequired = ["integrationId", "counterpartyId", "lines", "pricesIncludeVat"];
|
|
260
|
+
var paymentRequired = ["paymentMethod", "paymentDate"];
|
|
261
|
+
var invoiceItemKeys = ["counterpartyId", "lines", "pricesIncludeVat", "dueDate", "recipients"];
|
|
262
|
+
var amountMinor = { type: "integer", description: "amount in minor units; omit for the full remaining amount" };
|
|
263
|
+
var schema = (properties, req) => ({
|
|
264
|
+
type: "object",
|
|
265
|
+
properties,
|
|
266
|
+
...req ? { required: req } : {}
|
|
267
|
+
});
|
|
268
|
+
var draft = (a, ctx, body) => createDraft(ctx.config, ctx.now, required(a, "integrationId"), body);
|
|
269
|
+
var INTEGRATION_TOOLS = [
|
|
270
|
+
{
|
|
271
|
+
name: "list_integrations",
|
|
272
|
+
description: "List the business systems (invoicing software such as Invoice4U) connected to Financy, with each one's connection health and sync state. Start here to find an integrationId.",
|
|
273
|
+
inputSchema: schema({}),
|
|
274
|
+
run: (_a, ctx) => fetchIntegrations(ctx.config, ctx.now)
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: "get_integration",
|
|
278
|
+
description: "Get one connected business system: connection health, per-stream sync state and data freshness.",
|
|
279
|
+
inputSchema: schema({ integrationId }, ["integrationId"]),
|
|
280
|
+
run: (a, ctx) => fetchIntegration(ctx.config, ctx.now, required(a, "integrationId"))
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "get_receivables",
|
|
284
|
+
description: `Who owes the business money: open receivables across connected business systems, with totals and aging buckets per currency and per customer. Each integration entry says how fresh its data is and whether its first sync is still running. ${AMOUNTS}`,
|
|
285
|
+
inputSchema: schema({
|
|
286
|
+
integrationIds: { type: "array", items: integrationId, description: "limit to these integrations (default: all)" },
|
|
287
|
+
format: { type: "string", enum: ["concise", "detailed"], description: "detailed also lists each customer's open documents" },
|
|
288
|
+
asOf: localDate("the date to age receivables against, YYYY-MM-DD (default today)")
|
|
289
|
+
}),
|
|
290
|
+
run: (a, ctx) => fetchReceivables(ctx.config, ctx.now, {
|
|
291
|
+
integrations: Array.isArray(a.integrationIds) ? a.integrationIds.map(String) : void 0,
|
|
292
|
+
detailed: a.format === "detailed",
|
|
293
|
+
asOf: str2(a.asOf)
|
|
294
|
+
})
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: "list_invoicing_documents",
|
|
298
|
+
description: `List documents (invoices, receipts, credit notes, proformas and others) of one connected business system, filtered by status, type, customer or issue date. ${PAGING_NOTE} ${AMOUNTS}`,
|
|
299
|
+
inputSchema: schema(
|
|
300
|
+
{
|
|
301
|
+
integrationId,
|
|
302
|
+
status: { type: "string", enum: ["open", "partially_paid", "paid", "partially_credited", "credited", "cancelled", "closed", "unknown"] },
|
|
303
|
+
type: { type: "string", enum: ["invoice", "receipt", "invoice_receipt", "credit_note", "proforma", "quote", "order", "delivery_note", "other"] },
|
|
304
|
+
counterpartyId,
|
|
305
|
+
issuedFrom: localDate("issued on or after, YYYY-MM-DD"),
|
|
306
|
+
issuedTo: localDate("issued on or before, YYYY-MM-DD"),
|
|
307
|
+
receivableOnly: { type: "boolean", description: "only documents that are owed money" },
|
|
308
|
+
...PAGING2
|
|
309
|
+
},
|
|
310
|
+
["integrationId"]
|
|
311
|
+
),
|
|
312
|
+
run: (a, ctx) => fetchInvoicingDocuments(ctx.config, ctx.now, required(a, "integrationId"), {
|
|
313
|
+
status: str2(a.status),
|
|
314
|
+
type: str2(a.type),
|
|
315
|
+
customer: str2(a.counterpartyId),
|
|
316
|
+
from: str2(a.issuedFrom),
|
|
317
|
+
to: str2(a.issuedTo),
|
|
318
|
+
receivableOnly: bool2(a.receivableOnly),
|
|
319
|
+
limit: str2(a.limit),
|
|
320
|
+
cursor: str2(a.cursor),
|
|
321
|
+
all: bool2(a.all)
|
|
322
|
+
})
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "get_invoicing_document",
|
|
326
|
+
description: `Get one invoicing document with its amounts, status, links to related documents and what remains owed. ${AMOUNTS}`,
|
|
327
|
+
inputSchema: schema({ integrationId, documentId }, ["integrationId", "documentId"]),
|
|
328
|
+
run: (a, ctx) => fetchInvoicingDocument(ctx.config, ctx.now, required(a, "integrationId"), required(a, "documentId"))
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "list_invoicing_customers",
|
|
332
|
+
description: `List the customers of one connected business system, optionally matching part of the name. ${PAGING_NOTE}`,
|
|
333
|
+
inputSchema: schema(
|
|
334
|
+
{ integrationId, q: { type: "string", description: "part of the customer name" }, ...PAGING2 },
|
|
335
|
+
["integrationId"]
|
|
336
|
+
),
|
|
337
|
+
run: (a, ctx) => fetchInvoicingCustomers(ctx.config, ctx.now, required(a, "integrationId"), {
|
|
338
|
+
search: str2(a.q),
|
|
339
|
+
limit: str2(a.limit),
|
|
340
|
+
cursor: str2(a.cursor),
|
|
341
|
+
all: bool2(a.all)
|
|
342
|
+
})
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "get_invoicing_customer",
|
|
346
|
+
description: "Get one customer of a connected business system by id (from list_invoicing_customers).",
|
|
347
|
+
inputSchema: schema({ integrationId, counterpartyId }, ["integrationId", "counterpartyId"]),
|
|
348
|
+
run: (a, ctx) => fetchInvoicingCustomer(ctx.config, ctx.now, required(a, "integrationId"), required(a, "counterpartyId"))
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "list_proposals",
|
|
352
|
+
description: "List your document drafts, newest first, with their status (pending_approval until the user approves in Financy). Filter by integrationId or status; limit up to 50 (no cursor).",
|
|
353
|
+
inputSchema: schema({
|
|
354
|
+
integrationId,
|
|
355
|
+
status: { type: "string", enum: ["pending_approval", "approved", "executing", "completed", "partially_failed", "failed", "expired", "cancelled"] },
|
|
356
|
+
limit: { type: "number", description: "how many (1\u201350)" }
|
|
357
|
+
}),
|
|
358
|
+
run: (a, ctx) => fetchProposals(ctx.config, ctx.now, {
|
|
359
|
+
integration: str2(a.integrationId),
|
|
360
|
+
status: str2(a.status),
|
|
361
|
+
limit: str2(a.limit)
|
|
362
|
+
})
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: "get_proposal",
|
|
366
|
+
description: "Get a document draft, its approval link, previews and current status, including any issued document results once the user has approved it in Financy.",
|
|
367
|
+
inputSchema: schema({ proposalId }, ["proposalId"]),
|
|
368
|
+
run: (a, ctx) => fetchProposal(ctx.config, ctx.now, required(a, "proposalId"))
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: "draft_invoice",
|
|
372
|
+
description: `${DRAFT} Drafts a tax invoice to an existing customer from list_invoicing_customers.`,
|
|
373
|
+
inputSchema: schema(invoiceProperties, invoiceRequired),
|
|
374
|
+
run: (a, ctx) => draft(a, ctx, { action: "create_tax_invoice", items: [selected(a, invoiceItemKeys)] })
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: "draft_invoice_receipt",
|
|
378
|
+
description: `${DRAFT} Drafts an invoice-receipt, with its payment, to an existing customer from list_invoicing_customers.`,
|
|
379
|
+
inputSchema: schema({ ...invoiceProperties, ...paymentProperties }, [...invoiceRequired, ...paymentRequired]),
|
|
380
|
+
run: (a, ctx) => draft(a, ctx, {
|
|
381
|
+
action: "create_invoice_receipt",
|
|
382
|
+
items: [{ ...selected(a, invoiceItemKeys), payment: paymentOf(a) }]
|
|
383
|
+
})
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: "draft_receipt",
|
|
387
|
+
description: `${DRAFT} Drafts a receipt against an existing invoice.`,
|
|
388
|
+
inputSchema: schema(
|
|
389
|
+
{
|
|
390
|
+
integrationId,
|
|
391
|
+
targetDocumentId: documentId,
|
|
392
|
+
amountMinor,
|
|
393
|
+
withholdingMinor: { type: "integer", description: "withholding tax at source, minor units" },
|
|
394
|
+
...paymentProperties,
|
|
395
|
+
recipients
|
|
396
|
+
},
|
|
397
|
+
["integrationId", "targetDocumentId", ...paymentRequired]
|
|
398
|
+
),
|
|
399
|
+
run: (a, ctx) => draft(a, ctx, {
|
|
400
|
+
action: "create_receipt",
|
|
401
|
+
items: [{ ...selected(a, ["targetDocumentId", "amountMinor", "withholdingMinor", "recipients"]), payment: paymentOf(a) }]
|
|
402
|
+
})
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
name: "draft_credit_note",
|
|
406
|
+
description: `${DRAFT} Drafts credit notes against existing invoices, each for at most what remains uncredited.`,
|
|
407
|
+
inputSchema: schema(
|
|
408
|
+
{
|
|
409
|
+
integrationId,
|
|
410
|
+
recipients,
|
|
411
|
+
items: {
|
|
412
|
+
type: "array",
|
|
413
|
+
description: "1\u201350 credit notes",
|
|
414
|
+
items: {
|
|
415
|
+
type: "object",
|
|
416
|
+
properties: { targetDocumentId: documentId, amountMinor, reason: { type: "string" } },
|
|
417
|
+
required: ["targetDocumentId"]
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
["integrationId", "items"]
|
|
422
|
+
),
|
|
423
|
+
run: (a, ctx) => draft(a, ctx, {
|
|
424
|
+
action: "create_credit_note",
|
|
425
|
+
items: Array.isArray(a.items) ? a.items.map((item) => ({
|
|
426
|
+
...item !== null && typeof item === "object" ? item : {},
|
|
427
|
+
...a.recipients === void 0 ? {} : { recipients: a.recipients }
|
|
428
|
+
})) : []
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
];
|
|
432
|
+
var LOCAL_TOOLS = [...TOOLS, ...INTEGRATION_TOOLS];
|
|
433
|
+
var LOCAL_BY_NAME = new Map(LOCAL_TOOLS.map((t) => [t.name, t]));
|
|
434
|
+
function callLocalTool(name, args, io) {
|
|
435
|
+
return dispatchTool(LOCAL_BY_NAME, name, args, io);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/mcp/server.ts
|
|
439
|
+
async function startMcpServer(env) {
|
|
440
|
+
const server = new Server(
|
|
441
|
+
{ name: "financy", version: VERSION },
|
|
442
|
+
{ capabilities: { tools: {} } }
|
|
443
|
+
);
|
|
444
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
445
|
+
tools: LOCAL_TOOLS.map((t) => ({
|
|
446
|
+
name: t.name,
|
|
447
|
+
description: t.description,
|
|
448
|
+
inputSchema: t.inputSchema
|
|
449
|
+
}))
|
|
450
|
+
}));
|
|
451
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
452
|
+
const result = await callLocalTool(request.params.name, request.params.arguments ?? {}, { env });
|
|
453
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
454
|
+
});
|
|
455
|
+
await server.connect(new StdioServerTransport());
|
|
456
|
+
}
|
|
457
|
+
export {
|
|
458
|
+
startMcpServer
|
|
459
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "financy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Your Open-Finance accounts, balances and transactions in the terminal
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Your Open-Finance accounts, balances and transactions in the terminal — with an embedded MCP server and agent skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"financy": "dist/bin.js"
|
|
@@ -45,7 +45,7 @@ These are not optional and they apply to every step below.
|
|
|
45
45
|
The financy data API is a paid feature. To finish this skill the user must have:
|
|
46
46
|
|
|
47
47
|
1. A registered Financy account at <https://open-finance.ai>.
|
|
48
|
-
2. A **paid plan** — Starter or
|
|
48
|
+
2. A **paid plan** — Starter, Pro, or Ultra. The data API is not available on the free
|
|
49
49
|
plan.
|
|
50
50
|
3. Three values from the Financy app → **Settings → API**: `clientId`,
|
|
51
51
|
`clientSecret`, and `userId`.
|
|
@@ -143,6 +143,32 @@ If anything looks stale, do not refresh on your own initiative. Hand off to the
|
|
|
143
143
|
`freshness-check` skill, which knows how to weigh staleness against the 20-credit
|
|
144
144
|
cost.
|
|
145
145
|
|
|
146
|
+
## Business systems (invoicing)
|
|
147
|
+
|
|
148
|
+
The invoicing commands need CLI **0.4.0** or later (`financy --version`; `financy update` if older).
|
|
149
|
+
|
|
150
|
+
Besides bank and card data, the same credentials read a connected invoicing
|
|
151
|
+
system — Invoice4U today. If the user mentions invoices, receipts, customers or
|
|
152
|
+
"who owes me", check what is connected:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
financy integrations list --json
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- An empty list means nothing is connected yet. The user connects Invoice4U in the
|
|
159
|
+
Financy app, on the **Connections** page; the CLI cannot connect it.
|
|
160
|
+
- Exit `4` with `"code":"UPGRADE_REQUIRED"` means invoicing needs a paid plan
|
|
161
|
+
(Starter and above). Treat it like *Handling exit 4* below.
|
|
162
|
+
- An unknown-command error means the installed CLI predates these commands —
|
|
163
|
+
run `financy update`.
|
|
164
|
+
|
|
165
|
+
Once connected: `financy receivables` (who owes what), `financy documents list
|
|
166
|
+
<integrationId>`, `financy customers list <integrationId>`, and
|
|
167
|
+
`financy proposals list|get`. `financy proposals create <integrationId> --file
|
|
168
|
+
draft.json` only creates a **draft**: nothing is issued until the user approves it
|
|
169
|
+
in Financy at the printed approval link. Never describe a draft as an issued
|
|
170
|
+
invoice, and never try to approve it yourself — the CLI cannot.
|
|
171
|
+
|
|
146
172
|
## Handling exit 4
|
|
147
173
|
|
|
148
174
|
Exit `4` (`NOT_AVAILABLE_ON_PLAN`) means the account is registered and the
|
|
@@ -150,8 +176,8 @@ credentials are correct, but the plan does not include the data API.
|
|
|
150
176
|
|
|
151
177
|
Tell the user, in plain terms:
|
|
152
178
|
|
|
153
|
-
> Your credentials are valid, but the data API needs a paid plan (Starter
|
|
154
|
-
>
|
|
179
|
+
> Your credentials are valid, but the data API needs a paid plan (Starter, Pro,
|
|
180
|
+
> or Ultra). You can upgrade at <https://open-finance.ai> — the credentials are
|
|
155
181
|
> already saved, so `financy status` will work as soon as the plan is active.
|
|
156
182
|
|
|
157
183
|
Then stop. Do not retry the command, do not ask for different credentials, and
|
|
@@ -193,6 +219,9 @@ same surface is available as tools. Register it once:
|
|
|
193
219
|
claude mcp add financy -- npx financy mcp
|
|
194
220
|
```
|
|
195
221
|
|
|
196
|
-
The server resolves credentials exactly as the CLI does.
|
|
222
|
+
The server resolves credentials exactly as the CLI does. It also serves the
|
|
223
|
+
invoicing tools (`list_integrations`, `get_receivables`, `list_invoicing_documents`,
|
|
224
|
+
`draft_invoice`, …); a `draft_*` tool creates a draft the user must approve in
|
|
225
|
+
Financy before anything is issued. Until setup is
|
|
197
226
|
complete, every tool returns a structured `NOT_CONFIGURED` error — which is your
|
|
198
227
|
signal to run this skill.
|