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,117 @@
1
+ import type { Transaction, ReconciliationResult, MatchedPair } from '../types.js';
2
+
3
+ const DATE_TOLERANCE_DAYS = 2;
4
+
5
+ /**
6
+ * Levenshtein distance between two strings.
7
+ */
8
+ function levenshtein(a: string, b: string): number {
9
+ const m = a.length;
10
+ const n = b.length;
11
+ const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
12
+ Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
13
+ );
14
+
15
+ for (let i = 1; i <= m; i++) {
16
+ for (let j = 1; j <= n; j++) {
17
+ if (a[i - 1] === b[j - 1]) {
18
+ dp[i][j] = dp[i - 1][j - 1];
19
+ } else {
20
+ dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
21
+ }
22
+ }
23
+ }
24
+
25
+ return dp[m][n];
26
+ }
27
+
28
+ /**
29
+ * Normalized Levenshtein distance: 0 = identical, 1 = completely different.
30
+ */
31
+ function normalizedLevenshtein(a: string, b: string): number {
32
+ if (a.length === 0 && b.length === 0) return 0;
33
+ const maxLen = Math.max(a.length, b.length);
34
+ if (maxLen === 0) return 0;
35
+ return levenshtein(a, b) / maxLen;
36
+ }
37
+
38
+ /**
39
+ * Reconcile Stripe transactions against Xero transactions.
40
+ *
41
+ * Matching criteria:
42
+ * 1. Exact amount match (in cents)
43
+ * 2. Date within ±2 days
44
+ * 3. HARD GATE: description similarity >= 20% (Critical Fix 4)
45
+ * Prevents false positives like "AWS hosting $50" matching "Employee lunch $50"
46
+ *
47
+ * Confidence: weighted score (40% date proximity + 60% description similarity).
48
+ */
49
+ export function reconcile(
50
+ stripeTransactions: Transaction[],
51
+ xeroTransactions: Transaction[]
52
+ ): ReconciliationResult {
53
+ const matchedStripeIds = new Set<string>();
54
+ const matchedXeroIds = new Set<string>();
55
+ const matched: MatchedPair[] = [];
56
+
57
+ for (const st of stripeTransactions) {
58
+ const stDate = new Date(st.date).getTime();
59
+
60
+ let bestMatch: { xero: Transaction; confidence: number } | null = null;
61
+
62
+ for (const xt of xeroTransactions) {
63
+ if (matchedXeroIds.has(xt.id)) continue;
64
+
65
+ // 1. Exact amount match required
66
+ if (st.amount !== xt.amount) continue;
67
+
68
+ // 2. Date tolerance check
69
+ const xtDate = new Date(xt.date).getTime();
70
+ const daysDiff = Math.abs(stDate - xtDate) / (1000 * 60 * 60 * 24);
71
+ if (daysDiff > DATE_TOLERANCE_DAYS) continue;
72
+
73
+ // 3. HARD GATE: description similarity (Critical Fix 4)
74
+ const descSimilarity =
75
+ 1 -
76
+ normalizedLevenshtein(
77
+ st.description.toLowerCase(),
78
+ xt.description.toLowerCase()
79
+ );
80
+ if (descSimilarity < 0.2) continue;
81
+
82
+ // Confidence score
83
+ const dateScore = 1 - daysDiff / DATE_TOLERANCE_DAYS;
84
+ const confidence = dateScore * 0.4 + descSimilarity * 0.6;
85
+
86
+ if (!bestMatch || confidence > bestMatch.confidence) {
87
+ bestMatch = { xero: xt, confidence };
88
+ }
89
+ }
90
+
91
+ if (bestMatch) {
92
+ matchedStripeIds.add(st.id);
93
+ matchedXeroIds.add(bestMatch.xero.id);
94
+ matched.push({
95
+ stripe: st,
96
+ xero: bestMatch.xero,
97
+ confidence: Math.round(bestMatch.confidence * 1000) / 1000,
98
+ });
99
+ }
100
+ }
101
+
102
+ const unmatchedStripe = stripeTransactions.filter((t) => !matchedStripeIds.has(t.id));
103
+ const unmatchedXero = xeroTransactions.filter((t) => !matchedXeroIds.has(t.id));
104
+
105
+ return {
106
+ matched,
107
+ unmatched_stripe: unmatchedStripe,
108
+ unmatched_xero: unmatchedXero,
109
+ summary: {
110
+ total_stripe: stripeTransactions.length,
111
+ total_xero: xeroTransactions.length,
112
+ matched_count: matched.length,
113
+ unmatched_stripe_count: unmatchedStripe.length,
114
+ unmatched_xero_count: unmatchedXero.length,
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,98 @@
1
+ import type { Transaction, TaxSummary, TaxByRate } from '../types.js';
2
+
3
+ /**
4
+ * Default VAT/tax rates by jurisdiction.
5
+ * Key = jurisdiction code, value = array of applicable rates.
6
+ */
7
+ const TAX_RATES_BY_JURISDICTION: Record<string, number[]> = {
8
+ EU: [0.0, 0.05, 0.12, 0.2, 0.21, 0.25],
9
+ NL: [0.0, 0.09, 0.21],
10
+ DE: [0.0, 0.07, 0.19],
11
+ FR: [0.0, 0.055, 0.1, 0.2],
12
+ GB: [0.0, 0.05, 0.2],
13
+ US: [0.0, 0.05, 0.07, 0.08, 0.1],
14
+ DEFAULT: [0.0, 0.1, 0.2],
15
+ };
16
+
17
+ const STANDARD_RATE_BY_JURISDICTION: Record<string, number> = {
18
+ NL: 0.21,
19
+ DE: 0.19,
20
+ FR: 0.2,
21
+ GB: 0.2,
22
+ US: 0.08,
23
+ EU: 0.21,
24
+ DEFAULT: 0.2,
25
+ };
26
+
27
+ /**
28
+ * Calculate VAT/sales tax summary from transactions.
29
+ * Input amounts are in cents; output is in major currency units.
30
+ *
31
+ * This is a simplified v1 calculation. For production:
32
+ * - Use actual tax metadata from the payment processor
33
+ * - Apply jurisdiction-specific rules
34
+ */
35
+ export function calculateTax(
36
+ transactions: Transaction[],
37
+ period: 'quarter' | 'year',
38
+ jurisdiction?: string
39
+ ): TaxSummary {
40
+ const provider = transactions.length > 0 ? transactions[0].provider : 'stripe';
41
+ const jur = (jurisdiction ?? 'DEFAULT').toUpperCase();
42
+
43
+ // Standard rate for the jurisdiction
44
+ const standardRate = STANDARD_RATE_BY_JURISDICTION[jur] ?? STANDARD_RATE_BY_JURISDICTION['DEFAULT'];
45
+
46
+ // Income transactions are taxable
47
+ const taxableTransactions = transactions.filter((t) => t.type === 'income');
48
+
49
+ // Group by assumed tax rate (in v1, we assume all income at standard rate unless metadata says otherwise)
50
+ const byRate: Record<number, { taxable: number; tax: number }> = {};
51
+
52
+ for (const t of taxableTransactions) {
53
+ // Try to get tax rate from metadata
54
+ const rateStr = t.metadata?.tax_rate;
55
+ const rate = rateStr ? parseFloat(rateStr) : standardRate;
56
+
57
+ // Tax-exclusive: taxable amount = amount / (1 + rate), tax = amount - taxable
58
+ const taxableAmountCents = t.amount / (1 + rate);
59
+ const taxAmountCents = t.amount - taxableAmountCents;
60
+
61
+ if (!byRate[rate]) byRate[rate] = { taxable: 0, tax: 0 };
62
+ byRate[rate].taxable += taxableAmountCents;
63
+ byRate[rate].tax += taxAmountCents;
64
+ }
65
+
66
+ const byRateResult: TaxByRate[] = Object.entries(byRate).map(([rateStr, totals]) => {
67
+ const rate = parseFloat(rateStr);
68
+ return {
69
+ rate,
70
+ rate_label: rate === 0 ? 'Exempt' : `${(rate * 100).toFixed(0)}% VAT`,
71
+ taxable_amount: Math.round(totals.taxable) / 100,
72
+ tax_amount: Math.round(totals.tax) / 100,
73
+ };
74
+ });
75
+
76
+ const totalTaxableCents = Object.values(byRate).reduce((sum, r) => sum + r.taxable, 0);
77
+ const totalTaxCents = Object.values(byRate).reduce((sum, r) => sum + r.tax, 0);
78
+
79
+ // Filing period label
80
+ const now = new Date();
81
+ const year = now.getFullYear();
82
+ const quarter = Math.ceil((now.getMonth() + 1) / 3);
83
+ const filingPeriod =
84
+ period === 'year'
85
+ ? `${year}`
86
+ : `Q${quarter} ${year}`;
87
+
88
+ return {
89
+ provider,
90
+ period,
91
+ jurisdiction: jur !== 'DEFAULT' ? jur : undefined,
92
+ total_taxable_amount: Math.round(totalTaxableCents) / 100,
93
+ total_tax_collected: Math.round(totalTaxCents) / 100,
94
+ total_tax_owed: Math.round(totalTaxCents) / 100, // In v1, owed = collected
95
+ by_rate: byRateResult,
96
+ filing_period: filingPeriod,
97
+ };
98
+ }