financeops-mcp 0.1.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/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
- package/.github/assets/banner.svg +104 -0
- package/.github/pull_request_template.md +25 -0
- package/CODE_OF_CONDUCT.md +40 -0
- package/CONTRIBUTING.md +71 -0
- package/LICENSE +21 -0
- package/README.md +390 -0
- package/SECURITY.md +30 -0
- package/dist/adapters/stripe.d.ts +15 -0
- package/dist/adapters/stripe.js +175 -0
- package/dist/adapters/types.d.ts +30 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/xero.d.ts +19 -0
- package/dist/adapters/xero.js +261 -0
- package/dist/analysis/anomaly.d.ts +12 -0
- package/dist/analysis/anomaly.js +103 -0
- package/dist/analysis/cashflow.d.ts +10 -0
- package/dist/analysis/cashflow.js +134 -0
- package/dist/analysis/pnl.d.ts +8 -0
- package/dist/analysis/pnl.js +56 -0
- package/dist/analysis/reconciliation.d.ts +14 -0
- package/dist/analysis/reconciliation.js +98 -0
- package/dist/analysis/tax.d.ts +11 -0
- package/dist/analysis/tax.js +81 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +565 -0
- package/dist/lib/audit.d.ts +17 -0
- package/dist/lib/audit.js +70 -0
- package/dist/lib/providers.d.ts +6 -0
- package/dist/lib/providers.js +25 -0
- package/dist/premium/gate.d.ts +12 -0
- package/dist/premium/gate.js +22 -0
- package/dist/types.d.ts +138 -0
- package/dist/types.js +7 -0
- package/mcpize.yaml +10 -0
- package/package.json +35 -0
- package/src/adapters/stripe.ts +190 -0
- package/src/adapters/types.ts +34 -0
- package/src/adapters/xero.ts +317 -0
- package/src/analysis/anomaly.ts +119 -0
- package/src/analysis/cashflow.ts +158 -0
- package/src/analysis/pnl.ts +80 -0
- package/src/analysis/reconciliation.ts +117 -0
- package/src/analysis/tax.ts +98 -0
- package/src/index.ts +649 -0
- package/src/lib/audit.ts +92 -0
- package/src/lib/providers.ts +29 -0
- package/src/premium/gate.ts +24 -0
- package/src/types.ts +153 -0
- package/tests/adapters/stripe.test.ts +150 -0
- package/tests/adapters/xero.test.ts +188 -0
- package/tests/analysis/anomaly.test.ts +137 -0
- package/tests/analysis/cashflow.test.ts +112 -0
- package/tests/analysis/pnl.test.ts +95 -0
- package/tests/analysis/reconciliation.test.ts +121 -0
- package/tests/analysis/tax.test.ts +163 -0
- package/tests/helpers/mock-data.ts +135 -0
- package/tests/lib/audit.test.ts +89 -0
- package/tests/lib/providers.test.ts +129 -0
- package/tests/premium/cash_flow_forecast.test.ts +157 -0
- package/tests/premium/detect_anomalies.test.ts +189 -0
- package/tests/premium/gate.test.ts +59 -0
- package/tests/premium/generate_pnl.test.ts +155 -0
- package/tests/premium/multi_currency_report.test.ts +141 -0
- package/tests/premium/reconcile.test.ts +174 -0
- package/tests/premium/tax_summary.test.ts +166 -0
- package/tests/tools/expense_tracker.test.ts +181 -0
- package/tests/tools/financial_health.test.ts +196 -0
- package/tests/tools/get_balances.test.ts +160 -0
- package/tests/tools/list_invoices.test.ts +191 -0
- package/tests/tools/list_transactions.test.ts +210 -0
- package/tests/tools/revenue_summary.test.ts +188 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +9 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { logAudit, cleanup } from './lib/audit.js';
|
|
10
|
+
import { getProvider, getProviders } from './lib/providers.js';
|
|
11
|
+
import { generatePnL } from './analysis/pnl.js';
|
|
12
|
+
import { forecastCashFlow } from './analysis/cashflow.js';
|
|
13
|
+
import { reconcile } from './analysis/reconciliation.js';
|
|
14
|
+
import { detectAnomalies } from './analysis/anomaly.js';
|
|
15
|
+
import { calculateTax } from './analysis/tax.js';
|
|
16
|
+
import { requirePro, isPro } from './premium/gate.js';
|
|
17
|
+
|
|
18
|
+
// ─── Tool input schemas ───────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const ListTransactionsSchema = z.object({
|
|
21
|
+
provider: z.enum(['stripe', 'xero']),
|
|
22
|
+
date_from: z.string().optional(),
|
|
23
|
+
date_to: z.string().optional(),
|
|
24
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
25
|
+
cursor: z.string().optional(),
|
|
26
|
+
status: z.string().optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const GetBalancesSchema = z.object({
|
|
30
|
+
provider: z.enum(['stripe', 'xero']),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const ListInvoicesSchema = z.object({
|
|
34
|
+
provider: z.enum(['stripe', 'xero']),
|
|
35
|
+
status: z.enum(['paid', 'pending', 'overdue']).optional(),
|
|
36
|
+
date_from: z.string().optional(),
|
|
37
|
+
date_to: z.string().optional(),
|
|
38
|
+
cursor: z.string().optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const RevenueSummarySchema = z.object({
|
|
42
|
+
provider: z.enum(['stripe', 'xero']),
|
|
43
|
+
period: z.enum(['day', 'week', 'month', 'quarter', 'year']),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const ExpenseTrackerSchema = z.object({
|
|
47
|
+
provider: z.enum(['stripe', 'xero']),
|
|
48
|
+
period: z.string().optional(),
|
|
49
|
+
categories: z.array(z.string()).optional(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const FinancialHealthSchema = z.object({
|
|
53
|
+
providers: z.array(z.enum(['stripe', 'xero'])).min(1),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const GeneratePnLSchema = z.object({
|
|
57
|
+
providers: z.array(z.enum(['stripe', 'xero'])).min(1),
|
|
58
|
+
period: z.enum(['month', 'quarter', 'year']),
|
|
59
|
+
date_from: z.string().optional(),
|
|
60
|
+
date_to: z.string().optional(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const CashFlowForecastSchema = z.object({
|
|
64
|
+
providers: z.array(z.enum(['stripe', 'xero'])).min(1),
|
|
65
|
+
months_ahead: z.union([z.literal(3), z.literal(6), z.literal(12)]),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const ReconcileSchema = z.object({
|
|
69
|
+
date_from: z.string().optional(),
|
|
70
|
+
date_to: z.string().optional(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const DetectAnomaliesSchema = z.object({
|
|
74
|
+
providers: z.array(z.enum(['stripe', 'xero'])).min(1),
|
|
75
|
+
period: z.string().optional(),
|
|
76
|
+
sensitivity: z.enum(['low', 'medium', 'high']).optional(),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const TaxSummarySchema = z.object({
|
|
80
|
+
provider: z.enum(['stripe', 'xero']),
|
|
81
|
+
tax_period: z.enum(['quarter', 'year']),
|
|
82
|
+
jurisdiction: z.string().optional(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const MultiCurrencyReportSchema = z.object({
|
|
86
|
+
providers: z.array(z.enum(['stripe', 'xero'])).min(1),
|
|
87
|
+
base_currency: z.enum(['EUR', 'USD', 'GBP']),
|
|
88
|
+
date: z.string().optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ─── Server ──────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
const server = new Server(
|
|
94
|
+
{ name: 'financeops-mcp', version: '0.1.0' },
|
|
95
|
+
{ capabilities: { tools: {} } }
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// ─── List tools ──────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
101
|
+
const proTools = isPro()
|
|
102
|
+
? [
|
|
103
|
+
{
|
|
104
|
+
name: 'generate_pnl',
|
|
105
|
+
description: 'Generate a Profit & Loss statement (PRO)',
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
providers: { type: 'array', items: { type: 'string', enum: ['stripe', 'xero'] } },
|
|
110
|
+
period: { type: 'string', enum: ['month', 'quarter', 'year'] },
|
|
111
|
+
date_from: { type: 'string' },
|
|
112
|
+
date_to: { type: 'string' },
|
|
113
|
+
},
|
|
114
|
+
required: ['providers', 'period'],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'cash_flow_forecast',
|
|
119
|
+
description: 'Predict future cash flow based on historical patterns (PRO)',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
providers: { type: 'array', items: { type: 'string', enum: ['stripe', 'xero'] } },
|
|
124
|
+
months_ahead: { type: 'number', enum: [3, 6, 12] },
|
|
125
|
+
},
|
|
126
|
+
required: ['providers', 'months_ahead'],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'reconcile',
|
|
131
|
+
description: 'Cross-reference Stripe payments with Xero records (PRO)',
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: {
|
|
135
|
+
date_from: { type: 'string' },
|
|
136
|
+
date_to: { type: 'string' },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'detect_anomalies',
|
|
142
|
+
description: 'Find unusual financial activity (PRO)',
|
|
143
|
+
inputSchema: {
|
|
144
|
+
type: 'object',
|
|
145
|
+
properties: {
|
|
146
|
+
providers: { type: 'array', items: { type: 'string', enum: ['stripe', 'xero'] } },
|
|
147
|
+
period: { type: 'string' },
|
|
148
|
+
sensitivity: { type: 'string', enum: ['low', 'medium', 'high'] },
|
|
149
|
+
},
|
|
150
|
+
required: ['providers'],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'tax_summary',
|
|
155
|
+
description: 'VAT/sales tax calculation and summary (PRO)',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {
|
|
159
|
+
provider: { type: 'string', enum: ['stripe', 'xero'] },
|
|
160
|
+
tax_period: { type: 'string', enum: ['quarter', 'year'] },
|
|
161
|
+
jurisdiction: { type: 'string' },
|
|
162
|
+
},
|
|
163
|
+
required: ['provider', 'tax_period'],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'multi_currency_report',
|
|
168
|
+
description: 'Consolidated reporting across currencies (PRO)',
|
|
169
|
+
inputSchema: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: {
|
|
172
|
+
providers: { type: 'array', items: { type: 'string', enum: ['stripe', 'xero'] } },
|
|
173
|
+
base_currency: { type: 'string', enum: ['EUR', 'USD', 'GBP'] },
|
|
174
|
+
date: { type: 'string' },
|
|
175
|
+
},
|
|
176
|
+
required: ['providers', 'base_currency'],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
]
|
|
180
|
+
: [];
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
tools: [
|
|
184
|
+
{
|
|
185
|
+
name: 'list_transactions',
|
|
186
|
+
description: 'Fetch transactions from Stripe or Xero',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
provider: { type: 'string', enum: ['stripe', 'xero'] },
|
|
191
|
+
date_from: { type: 'string' },
|
|
192
|
+
date_to: { type: 'string' },
|
|
193
|
+
limit: { type: 'number' },
|
|
194
|
+
cursor: { type: 'string' },
|
|
195
|
+
status: { type: 'string' },
|
|
196
|
+
},
|
|
197
|
+
required: ['provider'],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'get_balances',
|
|
202
|
+
description: 'Get current account balances',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
provider: { type: 'string', enum: ['stripe', 'xero'] },
|
|
207
|
+
},
|
|
208
|
+
required: ['provider'],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'list_invoices',
|
|
213
|
+
description: 'Fetch invoices with status',
|
|
214
|
+
inputSchema: {
|
|
215
|
+
type: 'object',
|
|
216
|
+
properties: {
|
|
217
|
+
provider: { type: 'string', enum: ['stripe', 'xero'] },
|
|
218
|
+
status: { type: 'string', enum: ['paid', 'pending', 'overdue'] },
|
|
219
|
+
date_from: { type: 'string' },
|
|
220
|
+
date_to: { type: 'string' },
|
|
221
|
+
cursor: { type: 'string' },
|
|
222
|
+
},
|
|
223
|
+
required: ['provider'],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'revenue_summary',
|
|
228
|
+
description: 'Calculate revenue metrics',
|
|
229
|
+
inputSchema: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
properties: {
|
|
232
|
+
provider: { type: 'string', enum: ['stripe', 'xero'] },
|
|
233
|
+
period: { type: 'string', enum: ['day', 'week', 'month', 'quarter', 'year'] },
|
|
234
|
+
},
|
|
235
|
+
required: ['provider', 'period'],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'expense_tracker',
|
|
240
|
+
description: 'Categorize and summarize expenses',
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {
|
|
244
|
+
provider: { type: 'string', enum: ['stripe', 'xero'] },
|
|
245
|
+
period: { type: 'string' },
|
|
246
|
+
categories: { type: 'array', items: { type: 'string' } },
|
|
247
|
+
},
|
|
248
|
+
required: ['provider'],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: 'financial_health',
|
|
253
|
+
description: 'Quick financial health check: burn rate, runway, MRR, churn',
|
|
254
|
+
inputSchema: {
|
|
255
|
+
type: 'object',
|
|
256
|
+
properties: {
|
|
257
|
+
providers: { type: 'array', items: { type: 'string', enum: ['stripe', 'xero'] } },
|
|
258
|
+
},
|
|
259
|
+
required: ['providers'],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
...proTools,
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ─── Call tool ───────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
270
|
+
const { name, arguments: args } = request.params;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
switch (name) {
|
|
274
|
+
case 'list_transactions': {
|
|
275
|
+
const input = ListTransactionsSchema.parse(args);
|
|
276
|
+
const provider = getProvider(input.provider);
|
|
277
|
+
const result = await provider.getTransactions({
|
|
278
|
+
date_from: input.date_from,
|
|
279
|
+
date_to: input.date_to,
|
|
280
|
+
limit: input.limit,
|
|
281
|
+
cursor: input.cursor,
|
|
282
|
+
status: input.status,
|
|
283
|
+
});
|
|
284
|
+
logAudit({ tool: name, provider: input.provider, input_summary: JSON.stringify({ limit: input.limit }), success: true });
|
|
285
|
+
return {
|
|
286
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case 'get_balances': {
|
|
291
|
+
const input = GetBalancesSchema.parse(args);
|
|
292
|
+
const provider = getProvider(input.provider);
|
|
293
|
+
const balances = await provider.getBalances();
|
|
294
|
+
logAudit({ tool: name, provider: input.provider, success: true });
|
|
295
|
+
return {
|
|
296
|
+
content: [{ type: 'text', text: JSON.stringify(balances, null, 2) }],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
case 'list_invoices': {
|
|
301
|
+
const input = ListInvoicesSchema.parse(args);
|
|
302
|
+
const provider = getProvider(input.provider);
|
|
303
|
+
const result = await provider.getInvoices({
|
|
304
|
+
status: input.status,
|
|
305
|
+
date_from: input.date_from,
|
|
306
|
+
date_to: input.date_to,
|
|
307
|
+
cursor: input.cursor,
|
|
308
|
+
});
|
|
309
|
+
logAudit({ tool: name, provider: input.provider, success: true });
|
|
310
|
+
return {
|
|
311
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
case 'revenue_summary': {
|
|
316
|
+
const input = RevenueSummarySchema.parse(args);
|
|
317
|
+
const provider = getProvider(input.provider);
|
|
318
|
+
|
|
319
|
+
const periodDates = getPeriodDates(input.period);
|
|
320
|
+
const txResult = await provider.getTransactions(periodDates);
|
|
321
|
+
const incomeTransactions = txResult.data.filter((t) => t.type === 'income');
|
|
322
|
+
const totalRevenueCents = incomeTransactions.reduce((sum, t) => sum + t.amount, 0);
|
|
323
|
+
|
|
324
|
+
const byCustomer: Record<string, number> = {};
|
|
325
|
+
for (const t of incomeTransactions) {
|
|
326
|
+
const key = t.description;
|
|
327
|
+
byCustomer[key] = (byCustomer[key] ?? 0) + t.amount;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const topCustomers = Object.entries(byCustomer)
|
|
331
|
+
.sort(([, a], [, b]) => b - a)
|
|
332
|
+
.slice(0, 5)
|
|
333
|
+
.map(([name, amountCents]) => ({ name, amount: amountCents / 100 }));
|
|
334
|
+
|
|
335
|
+
const summary = {
|
|
336
|
+
period: input.period,
|
|
337
|
+
provider: input.provider,
|
|
338
|
+
total_revenue: totalRevenueCents / 100,
|
|
339
|
+
transaction_count: incomeTransactions.length,
|
|
340
|
+
average_transaction:
|
|
341
|
+
incomeTransactions.length > 0
|
|
342
|
+
? totalRevenueCents / incomeTransactions.length / 100
|
|
343
|
+
: 0,
|
|
344
|
+
top_customers: topCustomers,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
logAudit({ tool: name, provider: input.provider, success: true });
|
|
348
|
+
return {
|
|
349
|
+
content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
case 'expense_tracker': {
|
|
354
|
+
const input = ExpenseTrackerSchema.parse(args);
|
|
355
|
+
const provider = getProvider(input.provider);
|
|
356
|
+
|
|
357
|
+
const expResult = await provider.getExpenses({
|
|
358
|
+
categories: input.categories,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const byCategory: Record<string, number> = {};
|
|
362
|
+
for (const e of expResult.data) {
|
|
363
|
+
const cat = e.category ?? 'Uncategorized';
|
|
364
|
+
byCategory[cat] = (byCategory[cat] ?? 0) + e.amount;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const total = expResult.data.reduce((sum, e) => sum + e.amount, 0);
|
|
368
|
+
|
|
369
|
+
const result = {
|
|
370
|
+
provider: input.provider,
|
|
371
|
+
total_expenses: total / 100,
|
|
372
|
+
by_category: Object.fromEntries(
|
|
373
|
+
Object.entries(byCategory).map(([k, v]) => [k, v / 100])
|
|
374
|
+
),
|
|
375
|
+
count: expResult.data.length,
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
logAudit({ tool: name, provider: input.provider, success: true });
|
|
379
|
+
return {
|
|
380
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
case 'financial_health': {
|
|
385
|
+
const input = FinancialHealthSchema.parse(args);
|
|
386
|
+
const providers = getProviders(input.providers);
|
|
387
|
+
|
|
388
|
+
const health: Record<string, unknown> = {
|
|
389
|
+
providers: input.providers,
|
|
390
|
+
notes: [] as string[],
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
for (let i = 0; i < providers.length; i++) {
|
|
394
|
+
const provider = providers[i];
|
|
395
|
+
const providerName = input.providers[i];
|
|
396
|
+
|
|
397
|
+
const [balances, txResult] = await Promise.all([
|
|
398
|
+
provider.getBalances(),
|
|
399
|
+
provider.getTransactions(getPeriodDates('month')),
|
|
400
|
+
]);
|
|
401
|
+
|
|
402
|
+
const monthlyIncome = txResult.data
|
|
403
|
+
.filter((t) => t.type === 'income')
|
|
404
|
+
.reduce((sum, t) => sum + t.amount, 0);
|
|
405
|
+
const monthlyExpenses = txResult.data
|
|
406
|
+
.filter((t) => t.type === 'expense')
|
|
407
|
+
.reduce((sum, t) => sum + t.amount, 0);
|
|
408
|
+
|
|
409
|
+
const totalAvailable = balances.reduce((sum, b) => sum + b.available, 0);
|
|
410
|
+
const burnRate = monthlyExpenses / 100;
|
|
411
|
+
const runway = burnRate > 0 ? (totalAvailable / 100) / burnRate : null;
|
|
412
|
+
|
|
413
|
+
health[providerName] = {
|
|
414
|
+
total_balance: totalAvailable / 100,
|
|
415
|
+
monthly_income: monthlyIncome / 100,
|
|
416
|
+
monthly_expenses: monthlyExpenses / 100,
|
|
417
|
+
burn_rate: burnRate,
|
|
418
|
+
runway_months: runway !== null ? Math.round(runway * 10) / 10 : 'N/A',
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// Important Fix I4: note when subscription data is unavailable
|
|
422
|
+
if (!provider.getSubscriptions) {
|
|
423
|
+
(health.notes as string[]).push(
|
|
424
|
+
`Subscription data (MRR, churn) only available from Stripe.`
|
|
425
|
+
);
|
|
426
|
+
} else {
|
|
427
|
+
const subscriptions = await provider.getSubscriptions();
|
|
428
|
+
const activeSubs = subscriptions.filter((s) => s.status === 'active');
|
|
429
|
+
const mrr = activeSubs.reduce((sum, s) => {
|
|
430
|
+
const monthly = s.interval === 'year' ? s.amount / 12 : s.amount;
|
|
431
|
+
return sum + monthly;
|
|
432
|
+
}, 0);
|
|
433
|
+
const totalSubs = subscriptions.length;
|
|
434
|
+
const canceledSubs = subscriptions.filter((s) => s.status === 'canceled').length;
|
|
435
|
+
const churnRate = totalSubs > 0 ? (canceledSubs / totalSubs) * 100 : 0;
|
|
436
|
+
|
|
437
|
+
(health[providerName] as Record<string, unknown>).mrr = mrr / 100;
|
|
438
|
+
(health[providerName] as Record<string, unknown>).churn_rate_pct =
|
|
439
|
+
Math.round(churnRate * 10) / 10;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
logAudit({ tool: name, success: true });
|
|
444
|
+
return {
|
|
445
|
+
content: [{ type: 'text', text: JSON.stringify(health, null, 2) }],
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ─── Pro tools ────────────────────────────────────────────────────────
|
|
450
|
+
|
|
451
|
+
case 'generate_pnl': {
|
|
452
|
+
requirePro(name);
|
|
453
|
+
const input = GeneratePnLSchema.parse(args);
|
|
454
|
+
const periodDates = input.date_from && input.date_to
|
|
455
|
+
? { date_from: input.date_from, date_to: input.date_to }
|
|
456
|
+
: getPeriodDates(input.period);
|
|
457
|
+
|
|
458
|
+
const allTransactions = [];
|
|
459
|
+
const allExpenses = [];
|
|
460
|
+
|
|
461
|
+
for (const providerName of input.providers) {
|
|
462
|
+
const provider = getProvider(providerName);
|
|
463
|
+
const [txResult, expResult] = await Promise.all([
|
|
464
|
+
provider.getTransactions(periodDates),
|
|
465
|
+
provider.getExpenses({}),
|
|
466
|
+
]);
|
|
467
|
+
allTransactions.push(...txResult.data);
|
|
468
|
+
allExpenses.push(...expResult.data);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const pnl = generatePnL(
|
|
472
|
+
allTransactions,
|
|
473
|
+
allExpenses,
|
|
474
|
+
periodDates.date_from ?? '',
|
|
475
|
+
periodDates.date_to ?? ''
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
logAudit({ tool: name, success: true });
|
|
479
|
+
return {
|
|
480
|
+
content: [{ type: 'text', text: JSON.stringify(pnl, null, 2) }],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
case 'cash_flow_forecast': {
|
|
485
|
+
requirePro(name);
|
|
486
|
+
const input = CashFlowForecastSchema.parse(args);
|
|
487
|
+
|
|
488
|
+
const allTransactions = [];
|
|
489
|
+
for (const providerName of input.providers) {
|
|
490
|
+
const provider = getProvider(providerName);
|
|
491
|
+
const txResult = await provider.getTransactions({});
|
|
492
|
+
allTransactions.push(...txResult.data);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const forecast = forecastCashFlow(allTransactions, input.months_ahead);
|
|
496
|
+
logAudit({ tool: name, success: true });
|
|
497
|
+
return {
|
|
498
|
+
content: [{ type: 'text', text: JSON.stringify(forecast, null, 2) }],
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
case 'reconcile': {
|
|
503
|
+
requirePro(name);
|
|
504
|
+
const input = ReconcileSchema.parse(args);
|
|
505
|
+
|
|
506
|
+
const stripeProvider = getProvider('stripe');
|
|
507
|
+
const xeroProvider = getProvider('xero');
|
|
508
|
+
|
|
509
|
+
const [stripeResult, xeroResult] = await Promise.all([
|
|
510
|
+
stripeProvider.getTransactions({
|
|
511
|
+
date_from: input.date_from,
|
|
512
|
+
date_to: input.date_to,
|
|
513
|
+
}),
|
|
514
|
+
xeroProvider.getTransactions({
|
|
515
|
+
date_from: input.date_from,
|
|
516
|
+
date_to: input.date_to,
|
|
517
|
+
}),
|
|
518
|
+
]);
|
|
519
|
+
|
|
520
|
+
const result = reconcile(stripeResult.data, xeroResult.data);
|
|
521
|
+
logAudit({ tool: name, success: true });
|
|
522
|
+
return {
|
|
523
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
case 'detect_anomalies': {
|
|
528
|
+
requirePro(name);
|
|
529
|
+
const input = DetectAnomaliesSchema.parse(args);
|
|
530
|
+
|
|
531
|
+
const allTransactions = [];
|
|
532
|
+
for (const providerName of input.providers) {
|
|
533
|
+
const provider = getProvider(providerName);
|
|
534
|
+
const txResult = await provider.getTransactions({});
|
|
535
|
+
allTransactions.push(...txResult.data);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const anomalies = detectAnomalies(allTransactions, input.sensitivity ?? 'medium');
|
|
539
|
+
logAudit({ tool: name, success: true });
|
|
540
|
+
return {
|
|
541
|
+
content: [{ type: 'text', text: JSON.stringify(anomalies, null, 2) }],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
case 'tax_summary': {
|
|
546
|
+
requirePro(name);
|
|
547
|
+
const input = TaxSummarySchema.parse(args);
|
|
548
|
+
const provider = getProvider(input.provider);
|
|
549
|
+
const txResult = await provider.getTransactions({});
|
|
550
|
+
const summary = calculateTax(txResult.data, input.tax_period, input.jurisdiction);
|
|
551
|
+
logAudit({ tool: name, provider: input.provider, success: true });
|
|
552
|
+
return {
|
|
553
|
+
content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
case 'multi_currency_report': {
|
|
558
|
+
requirePro(name);
|
|
559
|
+
const input = MultiCurrencyReportSchema.parse(args);
|
|
560
|
+
// Placeholder — full FX logic handled separately
|
|
561
|
+
logAudit({ tool: name, success: true });
|
|
562
|
+
return {
|
|
563
|
+
content: [
|
|
564
|
+
{
|
|
565
|
+
type: 'text',
|
|
566
|
+
text: JSON.stringify({
|
|
567
|
+
message: 'multi_currency_report requires EXCHANGERATE_API_KEY. See docs.',
|
|
568
|
+
base_currency: input.base_currency,
|
|
569
|
+
providers: input.providers,
|
|
570
|
+
}),
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
default:
|
|
577
|
+
return {
|
|
578
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
579
|
+
isError: true,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
} catch (err: unknown) {
|
|
583
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
584
|
+
logAudit({ tool: name, success: false, error: message });
|
|
585
|
+
return {
|
|
586
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
587
|
+
isError: true,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
593
|
+
|
|
594
|
+
function getPeriodDates(period: string): { date_from: string; date_to: string } {
|
|
595
|
+
const now = new Date();
|
|
596
|
+
const to = now.toISOString().substring(0, 10);
|
|
597
|
+
|
|
598
|
+
let from: Date;
|
|
599
|
+
switch (period) {
|
|
600
|
+
case 'day':
|
|
601
|
+
from = new Date(now);
|
|
602
|
+
from.setDate(from.getDate() - 1);
|
|
603
|
+
break;
|
|
604
|
+
case 'week':
|
|
605
|
+
from = new Date(now);
|
|
606
|
+
from.setDate(from.getDate() - 7);
|
|
607
|
+
break;
|
|
608
|
+
case 'month':
|
|
609
|
+
from = new Date(now);
|
|
610
|
+
from.setMonth(from.getMonth() - 1);
|
|
611
|
+
break;
|
|
612
|
+
case 'quarter':
|
|
613
|
+
from = new Date(now);
|
|
614
|
+
from.setMonth(from.getMonth() - 3);
|
|
615
|
+
break;
|
|
616
|
+
case 'year':
|
|
617
|
+
from = new Date(now);
|
|
618
|
+
from.setFullYear(from.getFullYear() - 1);
|
|
619
|
+
break;
|
|
620
|
+
default:
|
|
621
|
+
from = new Date(now);
|
|
622
|
+
from.setMonth(from.getMonth() - 1);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
date_from: from.toISOString().substring(0, 10),
|
|
627
|
+
date_to: to,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
632
|
+
|
|
633
|
+
async function main() {
|
|
634
|
+
// Clean up audit entries older than 90 days (Important Fix I9: only here, not in getAuditLog)
|
|
635
|
+
try {
|
|
636
|
+
cleanup(90);
|
|
637
|
+
} catch {
|
|
638
|
+
// Audit cleanup failure should not prevent server start
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const transport = new StdioServerTransport();
|
|
642
|
+
await server.connect(transport);
|
|
643
|
+
console.error('[financeops-mcp] Server started');
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
main().catch((err) => {
|
|
647
|
+
console.error('[financeops-mcp] Fatal error:', err);
|
|
648
|
+
process.exit(1);
|
|
649
|
+
});
|