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.
Files changed (75) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  3. package/.github/assets/banner.svg +104 -0
  4. package/.github/pull_request_template.md +25 -0
  5. package/CODE_OF_CONDUCT.md +40 -0
  6. package/CONTRIBUTING.md +71 -0
  7. package/LICENSE +21 -0
  8. package/README.md +390 -0
  9. package/SECURITY.md +30 -0
  10. package/dist/adapters/stripe.d.ts +15 -0
  11. package/dist/adapters/stripe.js +175 -0
  12. package/dist/adapters/types.d.ts +30 -0
  13. package/dist/adapters/types.js +2 -0
  14. package/dist/adapters/xero.d.ts +19 -0
  15. package/dist/adapters/xero.js +261 -0
  16. package/dist/analysis/anomaly.d.ts +12 -0
  17. package/dist/analysis/anomaly.js +103 -0
  18. package/dist/analysis/cashflow.d.ts +10 -0
  19. package/dist/analysis/cashflow.js +134 -0
  20. package/dist/analysis/pnl.d.ts +8 -0
  21. package/dist/analysis/pnl.js +56 -0
  22. package/dist/analysis/reconciliation.d.ts +14 -0
  23. package/dist/analysis/reconciliation.js +98 -0
  24. package/dist/analysis/tax.d.ts +11 -0
  25. package/dist/analysis/tax.js +81 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.js +565 -0
  28. package/dist/lib/audit.d.ts +17 -0
  29. package/dist/lib/audit.js +70 -0
  30. package/dist/lib/providers.d.ts +6 -0
  31. package/dist/lib/providers.js +25 -0
  32. package/dist/premium/gate.d.ts +12 -0
  33. package/dist/premium/gate.js +22 -0
  34. package/dist/types.d.ts +138 -0
  35. package/dist/types.js +7 -0
  36. package/mcpize.yaml +10 -0
  37. package/package.json +35 -0
  38. package/src/adapters/stripe.ts +190 -0
  39. package/src/adapters/types.ts +34 -0
  40. package/src/adapters/xero.ts +317 -0
  41. package/src/analysis/anomaly.ts +119 -0
  42. package/src/analysis/cashflow.ts +158 -0
  43. package/src/analysis/pnl.ts +80 -0
  44. package/src/analysis/reconciliation.ts +117 -0
  45. package/src/analysis/tax.ts +98 -0
  46. package/src/index.ts +649 -0
  47. package/src/lib/audit.ts +92 -0
  48. package/src/lib/providers.ts +29 -0
  49. package/src/premium/gate.ts +24 -0
  50. package/src/types.ts +153 -0
  51. package/tests/adapters/stripe.test.ts +150 -0
  52. package/tests/adapters/xero.test.ts +188 -0
  53. package/tests/analysis/anomaly.test.ts +137 -0
  54. package/tests/analysis/cashflow.test.ts +112 -0
  55. package/tests/analysis/pnl.test.ts +95 -0
  56. package/tests/analysis/reconciliation.test.ts +121 -0
  57. package/tests/analysis/tax.test.ts +163 -0
  58. package/tests/helpers/mock-data.ts +135 -0
  59. package/tests/lib/audit.test.ts +89 -0
  60. package/tests/lib/providers.test.ts +129 -0
  61. package/tests/premium/cash_flow_forecast.test.ts +157 -0
  62. package/tests/premium/detect_anomalies.test.ts +189 -0
  63. package/tests/premium/gate.test.ts +59 -0
  64. package/tests/premium/generate_pnl.test.ts +155 -0
  65. package/tests/premium/multi_currency_report.test.ts +141 -0
  66. package/tests/premium/reconcile.test.ts +174 -0
  67. package/tests/premium/tax_summary.test.ts +166 -0
  68. package/tests/tools/expense_tracker.test.ts +181 -0
  69. package/tests/tools/financial_health.test.ts +196 -0
  70. package/tests/tools/get_balances.test.ts +160 -0
  71. package/tests/tools/list_invoices.test.ts +191 -0
  72. package/tests/tools/list_transactions.test.ts +210 -0
  73. package/tests/tools/revenue_summary.test.ts +188 -0
  74. package/tsconfig.json +20 -0
  75. package/vitest.config.ts +9 -0
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Premium gate — reads PRO_LICENSE dynamically from environment.
3
+ * CRITICAL: Do NOT cache the env variable at module load time.
4
+ * Must re-read process.env.PRO_LICENSE on every call to support
5
+ * runtime license changes (e.g., in tests or when env is updated).
6
+ */
7
+ export declare class ProRequiredError extends Error {
8
+ constructor(tool: string);
9
+ }
10
+ export declare function isPro(): boolean;
11
+ export declare function requirePro(tool: string): void;
12
+ //# sourceMappingURL=gate.d.ts.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Premium gate — reads PRO_LICENSE dynamically from environment.
3
+ * CRITICAL: Do NOT cache the env variable at module load time.
4
+ * Must re-read process.env.PRO_LICENSE on every call to support
5
+ * runtime license changes (e.g., in tests or when env is updated).
6
+ */
7
+ export class ProRequiredError extends Error {
8
+ constructor(tool) {
9
+ super(`Tool "${tool}" requires a PRO_LICENSE. Set the PRO_LICENSE environment variable.`);
10
+ this.name = 'ProRequiredError';
11
+ }
12
+ }
13
+ export function isPro() {
14
+ // Dynamic read — NOT cached
15
+ return Boolean(process.env.PRO_LICENSE);
16
+ }
17
+ export function requirePro(tool) {
18
+ if (!isPro()) {
19
+ throw new ProRequiredError(tool);
20
+ }
21
+ }
22
+ //# sourceMappingURL=gate.js.map
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Core domain types for FinanceOps MCP.
3
+ * All monetary amounts are in the smallest currency unit (cents).
4
+ * Example: 15000 = €150.00
5
+ */
6
+ export interface Transaction {
7
+ id: string;
8
+ date: string;
9
+ amount: number;
10
+ currency: string;
11
+ description: string;
12
+ category?: string;
13
+ type: 'income' | 'expense' | 'transfer';
14
+ provider: 'stripe' | 'xero';
15
+ metadata?: Record<string, string>;
16
+ }
17
+ export interface Invoice {
18
+ id: string;
19
+ number: string;
20
+ customer_name: string;
21
+ customer_email?: string;
22
+ amount: number;
23
+ currency: string;
24
+ status: 'paid' | 'pending' | 'overdue' | 'draft' | 'voided';
25
+ due_date: string;
26
+ issued_date: string;
27
+ paid_date?: string;
28
+ provider: 'stripe' | 'xero';
29
+ readonly amount_due?: number;
30
+ readonly amount_paid?: number;
31
+ readonly amount_credited?: number;
32
+ }
33
+ export interface Balance {
34
+ currency: string;
35
+ available: number;
36
+ pending: number;
37
+ provider: 'stripe' | 'xero';
38
+ }
39
+ export interface Expense {
40
+ id: string;
41
+ date: string;
42
+ amount: number;
43
+ currency: string;
44
+ description: string;
45
+ category: string;
46
+ provider: 'stripe' | 'xero';
47
+ }
48
+ export interface Subscription {
49
+ id: string;
50
+ customer_name: string;
51
+ amount: number;
52
+ currency: string;
53
+ interval: 'month' | 'year';
54
+ status: 'active' | 'canceled' | 'past_due' | 'trialing';
55
+ current_period_start: string;
56
+ current_period_end: string;
57
+ provider: 'stripe' | 'xero';
58
+ }
59
+ export interface PaginatedResult<T> {
60
+ data: T[];
61
+ has_more: boolean;
62
+ next_cursor?: string;
63
+ }
64
+ export interface PnLReport {
65
+ period: string;
66
+ date_from: string;
67
+ date_to: string;
68
+ currency: string;
69
+ revenue: number;
70
+ cogs: number;
71
+ gross_profit: number;
72
+ gross_margin_pct: number;
73
+ operating_expenses: number;
74
+ net_income: number;
75
+ by_category: Record<string, number>;
76
+ }
77
+ export interface CashFlowForecast {
78
+ period_months: number;
79
+ generated_at: string;
80
+ historical_monthly_income: number;
81
+ historical_monthly_expenses: number;
82
+ trend_pct: number;
83
+ scenarios: {
84
+ conservative: MonthlyForecast[];
85
+ realistic: MonthlyForecast[];
86
+ optimistic: MonthlyForecast[];
87
+ };
88
+ }
89
+ export interface MonthlyForecast {
90
+ month: string;
91
+ income: number;
92
+ expenses: number;
93
+ net: number;
94
+ cumulative_net: number;
95
+ }
96
+ export interface ReconciliationResult {
97
+ matched: MatchedPair[];
98
+ unmatched_stripe: Transaction[];
99
+ unmatched_xero: Transaction[];
100
+ summary: {
101
+ total_stripe: number;
102
+ total_xero: number;
103
+ matched_count: number;
104
+ unmatched_stripe_count: number;
105
+ unmatched_xero_count: number;
106
+ };
107
+ }
108
+ export interface MatchedPair {
109
+ stripe: Transaction;
110
+ xero: Transaction;
111
+ confidence: number;
112
+ }
113
+ export interface Anomaly {
114
+ id: string;
115
+ type: 'duplicate' | 'large_variance' | 'missed_recurring' | 'unusual_timing';
116
+ severity: 'low' | 'medium' | 'high';
117
+ transaction?: Transaction;
118
+ description: string;
119
+ amount?: number;
120
+ date?: string;
121
+ }
122
+ export interface TaxSummary {
123
+ provider: 'stripe' | 'xero';
124
+ period: 'quarter' | 'year';
125
+ jurisdiction?: string;
126
+ total_taxable_amount: number;
127
+ total_tax_collected: number;
128
+ total_tax_owed: number;
129
+ by_rate: TaxByRate[];
130
+ filing_period: string;
131
+ }
132
+ export interface TaxByRate {
133
+ rate: number;
134
+ rate_label: string;
135
+ taxable_amount: number;
136
+ tax_amount: number;
137
+ }
138
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Core domain types for FinanceOps MCP.
3
+ * All monetary amounts are in the smallest currency unit (cents).
4
+ * Example: 15000 = €150.00
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
package/mcpize.yaml ADDED
@@ -0,0 +1,10 @@
1
+ version: 1
2
+ name: financeops-mcp
3
+ runtime: typescript
4
+ entry: dist/index.js
5
+ build:
6
+ install: npm ci
7
+ command: npx tsc
8
+ startCommand:
9
+ type: stdio
10
+ command: node dist/index.js
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "financeops-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Unified financial intelligence MCP server — Stripe + Xero",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "financeops-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.0.0",
20
+ "better-sqlite3": "^9.4.3",
21
+ "stripe": "^14.21.0",
22
+ "zod": "^3.22.4"
23
+ },
24
+ "devDependencies": {
25
+ "@types/better-sqlite3": "^7.6.8",
26
+ "@types/node": "^20.11.5",
27
+ "typescript": "^5.3.3",
28
+ "vitest": "^1.2.2"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "keywords": ["mcp", "finance", "stripe", "xero", "accounting"],
34
+ "license": "MIT"
35
+ }
@@ -0,0 +1,190 @@
1
+ import Stripe from 'stripe';
2
+ import type { FinanceProvider, TransactionQuery, InvoiceQuery, ExpenseQuery } from './types.js';
3
+ import type { Transaction, Invoice, Balance, Expense, Subscription, PaginatedResult } from '../types.js';
4
+
5
+ export class StripeAdapter implements FinanceProvider {
6
+ private client: Stripe;
7
+
8
+ constructor() {
9
+ const key = process.env.STRIPE_SECRET_KEY;
10
+ if (!key) {
11
+ throw new Error('STRIPE_SECRET_KEY environment variable is required');
12
+ }
13
+ this.client = new Stripe(key, { apiVersion: '2023-10-16' });
14
+ }
15
+
16
+ async getTransactions(opts: TransactionQuery): Promise<PaginatedResult<Transaction>> {
17
+ const params: Stripe.BalanceTransactionListParams = {
18
+ limit: Math.min(opts.limit ?? 100, 100),
19
+ };
20
+
21
+ if (opts.cursor) params.starting_after = opts.cursor;
22
+ if (opts.date_from) params.created = { gte: Math.floor(new Date(opts.date_from).getTime() / 1000) };
23
+ if (opts.date_to) {
24
+ params.created = {
25
+ ...(typeof params.created === 'object' && params.created !== null ? params.created : {}),
26
+ lte: Math.floor(new Date(opts.date_to).getTime() / 1000),
27
+ };
28
+ }
29
+
30
+ const result = await this.client.balanceTransactions.list(params);
31
+
32
+ const transactions: Transaction[] = result.data.map((bt) => ({
33
+ id: bt.id,
34
+ date: new Date(bt.created * 1000).toISOString(),
35
+ amount: bt.amount, // Stripe already returns amounts in cents
36
+ currency: bt.currency,
37
+ description: bt.description ?? bt.type,
38
+ category: bt.type,
39
+ type: this.mapStripeType(bt.type),
40
+ provider: 'stripe' as const,
41
+ metadata: undefined,
42
+ }));
43
+
44
+ return {
45
+ data: transactions,
46
+ has_more: result.has_more,
47
+ next_cursor: result.has_more ? result.data[result.data.length - 1]?.id : undefined,
48
+ };
49
+ }
50
+
51
+ async getBalances(): Promise<Balance[]> {
52
+ const balance = await this.client.balance.retrieve();
53
+
54
+ const balances: Balance[] = [];
55
+
56
+ for (const available of balance.available) {
57
+ const pending = balance.pending.find((p) => p.currency === available.currency);
58
+ balances.push({
59
+ currency: available.currency,
60
+ available: available.amount, // Already in cents
61
+ pending: pending?.amount ?? 0,
62
+ provider: 'stripe' as const,
63
+ });
64
+ }
65
+
66
+ return balances;
67
+ }
68
+
69
+ async getInvoices(opts: InvoiceQuery): Promise<PaginatedResult<Invoice>> {
70
+ const params: Stripe.InvoiceListParams = {
71
+ limit: 100,
72
+ };
73
+
74
+ if (opts.cursor) params.starting_after = opts.cursor;
75
+
76
+ if (opts.status) {
77
+ if (opts.status === 'paid') params.status = 'paid';
78
+ else if (opts.status === 'pending') params.status = 'open';
79
+ // 'overdue' = open past due_date; we post-filter below
80
+ }
81
+
82
+ const result = await this.client.invoices.list(params);
83
+
84
+ let invoices: Invoice[] = result.data.map((inv) => ({
85
+ id: inv.id,
86
+ number: inv.number ?? '',
87
+ customer_name: typeof inv.customer_name === 'string' ? inv.customer_name : '',
88
+ customer_email: typeof inv.customer_email === 'string' ? inv.customer_email : undefined,
89
+ amount: inv.amount_due, // Already in cents
90
+ currency: inv.currency,
91
+ status: this.mapStripeInvoiceStatus(inv),
92
+ due_date: inv.due_date ? new Date(inv.due_date * 1000).toISOString() : '',
93
+ issued_date: new Date(inv.created * 1000).toISOString(),
94
+ paid_date: inv.status_transitions?.paid_at
95
+ ? new Date(inv.status_transitions.paid_at * 1000).toISOString()
96
+ : undefined,
97
+ provider: 'stripe' as const,
98
+ }));
99
+
100
+ if (opts.date_from) {
101
+ const from = new Date(opts.date_from).getTime();
102
+ invoices = invoices.filter((inv) => new Date(inv.issued_date).getTime() >= from);
103
+ }
104
+ if (opts.date_to) {
105
+ const to = new Date(opts.date_to).getTime();
106
+ invoices = invoices.filter((inv) => new Date(inv.issued_date).getTime() <= to);
107
+ }
108
+ if (opts.status === 'overdue') {
109
+ const now = Date.now();
110
+ invoices = invoices.filter(
111
+ (inv) => inv.status !== 'paid' && inv.due_date && new Date(inv.due_date).getTime() < now
112
+ );
113
+ }
114
+
115
+ return {
116
+ data: invoices,
117
+ has_more: result.has_more,
118
+ next_cursor: result.has_more ? result.data[result.data.length - 1]?.id : undefined,
119
+ };
120
+ }
121
+
122
+ async getExpenses(opts: ExpenseQuery): Promise<PaginatedResult<Expense>> {
123
+ // Stripe expenses = charges (fees), refunds, and payouts
124
+ const params: Stripe.BalanceTransactionListParams = {
125
+ limit: 100,
126
+ type: 'payout',
127
+ };
128
+
129
+ if (opts.cursor) params.starting_after = opts.cursor;
130
+
131
+ const result = await this.client.balanceTransactions.list(params);
132
+
133
+ const expenses: Expense[] = result.data.map((bt) => ({
134
+ id: bt.id,
135
+ date: new Date(bt.created * 1000).toISOString(),
136
+ amount: Math.abs(bt.amount), // Cents
137
+ currency: bt.currency,
138
+ description: bt.description ?? bt.type,
139
+ category: bt.type === 'payout' ? 'Payouts' : bt.type,
140
+ provider: 'stripe' as const,
141
+ }));
142
+
143
+ return {
144
+ data: expenses,
145
+ has_more: result.has_more,
146
+ next_cursor: result.has_more ? result.data[result.data.length - 1]?.id : undefined,
147
+ };
148
+ }
149
+
150
+ async getSubscriptions(): Promise<Subscription[]> {
151
+ const result = await this.client.subscriptions.list({ limit: 100, status: 'all' });
152
+
153
+ return result.data.map((sub) => ({
154
+ id: sub.id,
155
+ customer_name: typeof sub.customer === 'string' ? sub.customer : (sub.customer as Stripe.Customer)?.name ?? '',
156
+ amount: sub.items.data.reduce((sum, item) => sum + (item.price.unit_amount ?? 0), 0),
157
+ currency: sub.currency,
158
+ interval: (sub.items.data[0]?.price.recurring?.interval ?? 'month') as 'month' | 'year',
159
+ status: this.mapStripeSubStatus(sub.status),
160
+ current_period_start: new Date(sub.current_period_start * 1000).toISOString(),
161
+ current_period_end: new Date(sub.current_period_end * 1000).toISOString(),
162
+ provider: 'stripe' as const,
163
+ }));
164
+ }
165
+
166
+ private mapStripeType(type: string): 'income' | 'expense' | 'transfer' {
167
+ if (['charge', 'payment', 'adjustment'].includes(type)) return 'income';
168
+ if (['refund', 'payout', 'stripe_fee', 'application_fee'].includes(type)) return 'expense';
169
+ return 'transfer';
170
+ }
171
+
172
+ private mapStripeInvoiceStatus(inv: Stripe.Invoice): 'paid' | 'pending' | 'overdue' | 'draft' | 'voided' {
173
+ if (inv.status === 'paid') return 'paid';
174
+ if (inv.status === 'draft') return 'draft';
175
+ if (inv.status === 'void') return 'voided';
176
+ if (inv.status === 'open') {
177
+ if (inv.due_date && Date.now() > inv.due_date * 1000) return 'overdue';
178
+ return 'pending';
179
+ }
180
+ return 'pending';
181
+ }
182
+
183
+ private mapStripeSubStatus(status: Stripe.Subscription.Status): 'active' | 'canceled' | 'past_due' | 'trialing' {
184
+ if (status === 'active') return 'active';
185
+ if (status === 'canceled') return 'canceled';
186
+ if (status === 'past_due' || status === 'unpaid') return 'past_due';
187
+ if (status === 'trialing') return 'trialing';
188
+ return 'active';
189
+ }
190
+ }
@@ -0,0 +1,34 @@
1
+ import type { Transaction, Invoice, Balance, Expense, Subscription, PaginatedResult } from '../types.js';
2
+
3
+ export type { Transaction, Invoice, Balance, Expense, Subscription, PaginatedResult };
4
+
5
+ export interface TransactionQuery {
6
+ date_from?: string;
7
+ date_to?: string;
8
+ limit?: number;
9
+ cursor?: string;
10
+ status?: string;
11
+ }
12
+
13
+ export interface InvoiceQuery {
14
+ status?: 'paid' | 'pending' | 'overdue';
15
+ date_from?: string;
16
+ date_to?: string;
17
+ cursor?: string;
18
+ }
19
+
20
+ export interface ExpenseQuery {
21
+ period?: string;
22
+ categories?: string[];
23
+ date_from?: string;
24
+ date_to?: string;
25
+ cursor?: string;
26
+ }
27
+
28
+ export interface FinanceProvider {
29
+ getTransactions(opts: TransactionQuery): Promise<PaginatedResult<Transaction>>;
30
+ getBalances(): Promise<Balance[]>;
31
+ getInvoices(opts: InvoiceQuery): Promise<PaginatedResult<Invoice>>;
32
+ getExpenses(opts: ExpenseQuery): Promise<PaginatedResult<Expense>>;
33
+ getSubscriptions?(): Promise<Subscription[]>;
34
+ }