@mintline/mcp 1.0.0 → 1.1.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.
Files changed (2) hide show
  1. package/dist/index.js +207 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21,9 +21,15 @@ function createClient(apiKey2) {
21
21
  },
22
22
  body: body ? JSON.stringify(body) : void 0
23
23
  });
24
- const data = await res.json();
24
+ let data;
25
+ try {
26
+ data = await res.json();
27
+ } catch {
28
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
29
+ }
25
30
  if (!data.success) {
26
- throw new Error(data.error?.message || "Request failed");
31
+ const errorMsg = data.error?.message || data.message || `Request failed (${res.status})`;
32
+ throw new Error(errorMsg);
27
33
  }
28
34
  return data;
29
35
  }
@@ -77,6 +83,32 @@ function createClient(apiKey2) {
77
83
  },
78
84
  async rejectMatch(id, reason) {
79
85
  return request("POST", `/api/matches/${id}/reject`, { reason });
86
+ },
87
+ // Analytics
88
+ async getSpendingSummary(params = {}) {
89
+ const query = new URLSearchParams();
90
+ if (params.groupBy) query.set("groupBy", params.groupBy);
91
+ if (params.dateFrom) query.set("dateFrom", params.dateFrom);
92
+ if (params.dateTo) query.set("dateTo", params.dateTo);
93
+ if (params.vendorId) query.set("vendorId", params.vendorId);
94
+ if (params.limit) query.set("limit", params.limit);
95
+ const qs = query.toString();
96
+ return request("GET", `/api/analytics/spending${qs ? `?${qs}` : ""}`);
97
+ },
98
+ async getTopVendors(params = {}) {
99
+ const query = new URLSearchParams();
100
+ if (params.limit) query.set("limit", params.limit);
101
+ const qs = query.toString();
102
+ return request("GET", `/api/analytics/top-vendors${qs ? `?${qs}` : ""}`);
103
+ },
104
+ async getSpendingTrends(params = {}) {
105
+ const query = new URLSearchParams();
106
+ if (params.months) query.set("months", params.months);
107
+ const qs = query.toString();
108
+ return request("GET", `/api/analytics/trends${qs ? `?${qs}` : ""}`);
109
+ },
110
+ async getUnmatchedSummary() {
111
+ return request("GET", `/api/analytics/unmatched`);
80
112
  }
81
113
  };
82
114
  }
@@ -221,6 +253,70 @@ var tools = [
221
253
  },
222
254
  required: ["id"]
223
255
  }
256
+ },
257
+ {
258
+ name: "spending_summary",
259
+ description: "Get spending summary with flexible grouping. Use this to answer questions like 'How much did I spend this month?' or 'What are my expenses by vendor?'",
260
+ inputSchema: {
261
+ type: "object",
262
+ properties: {
263
+ groupBy: {
264
+ type: "string",
265
+ enum: ["total", "vendor", "month", "week", "day"],
266
+ description: "How to group the spending data (default: total)"
267
+ },
268
+ dateFrom: {
269
+ type: "string",
270
+ description: "Start date (YYYY-MM-DD)"
271
+ },
272
+ dateTo: {
273
+ type: "string",
274
+ description: "End date (YYYY-MM-DD)"
275
+ },
276
+ vendorId: {
277
+ type: "string",
278
+ description: "Filter by specific vendor ID"
279
+ },
280
+ limit: {
281
+ type: "number",
282
+ description: "Max results for grouped queries (default 20)"
283
+ }
284
+ }
285
+ }
286
+ },
287
+ {
288
+ name: "top_vendors",
289
+ description: "Get top vendors ranked by total spending. Use this to answer 'Who are my biggest vendors?' or 'Where do I spend the most?'",
290
+ inputSchema: {
291
+ type: "object",
292
+ properties: {
293
+ limit: {
294
+ type: "number",
295
+ description: "Number of vendors to return (default 10)"
296
+ }
297
+ }
298
+ }
299
+ },
300
+ {
301
+ name: "spending_trends",
302
+ description: "Get monthly spending trends over time. Use this to answer 'How has my spending changed?' or 'Show me spending trends'",
303
+ inputSchema: {
304
+ type: "object",
305
+ properties: {
306
+ months: {
307
+ type: "number",
308
+ description: "Number of months to include (default 6)"
309
+ }
310
+ }
311
+ }
312
+ },
313
+ {
314
+ name: "unmatched_summary",
315
+ description: "Get a summary of items needing attention: unmatched receipts, transactions without receipts, and proposed matches to review. Use this for 'What needs my attention?' or 'Do I have unmatched expenses?'",
316
+ inputSchema: {
317
+ type: "object",
318
+ properties: {}
319
+ }
224
320
  }
225
321
  ];
226
322
  async function handleTool(client2, name, args) {
@@ -271,6 +367,32 @@ async function handleTool(client2, name, args) {
271
367
  await client2.rejectMatch(args.id, args.reason);
272
368
  return `Match ${args.id} rejected.${args.reason ? ` Reason: ${args.reason}` : ""}`;
273
369
  }
370
+ case "spending_summary": {
371
+ const result = await client2.getSpendingSummary({
372
+ groupBy: args.groupBy || "total",
373
+ dateFrom: args.dateFrom,
374
+ dateTo: args.dateTo,
375
+ vendorId: args.vendorId,
376
+ limit: args.limit || 20
377
+ });
378
+ return formatSpendingSummary(result.data, args.groupBy || "total");
379
+ }
380
+ case "top_vendors": {
381
+ const result = await client2.getTopVendors({
382
+ limit: args.limit || 10
383
+ });
384
+ return formatTopVendors(result.data.vendors);
385
+ }
386
+ case "spending_trends": {
387
+ const result = await client2.getSpendingTrends({
388
+ months: args.months || 6
389
+ });
390
+ return formatSpendingTrends(result.data.trends);
391
+ }
392
+ case "unmatched_summary": {
393
+ const result = await client2.getUnmatchedSummary();
394
+ return formatUnmatchedSummary(result.data);
395
+ }
274
396
  default:
275
397
  throw new Error(`Unknown tool: ${name}`);
276
398
  }
@@ -335,6 +457,89 @@ function formatMatches(matches) {
335
457
  (m) => `\u2022 ${m.id}: Receipt ${m.receiptId} \u2194 Transaction ${m.transactionId} (${Math.round(m.confidenceScore * 100)}% confidence) [${m.status}]`
336
458
  ).join("\n");
337
459
  }
460
+ function formatCurrency(amount) {
461
+ return new Intl.NumberFormat("en-US", {
462
+ style: "currency",
463
+ currency: "USD"
464
+ }).format(amount);
465
+ }
466
+ function formatSpendingSummary(data, groupBy) {
467
+ if (groupBy === "total" && data.summary) {
468
+ const s = data.summary;
469
+ let text2 = `Spending Summary (${data.period.from} to ${data.period.to})
470
+ `;
471
+ text2 += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
472
+ `;
473
+ text2 += `Total Spent: ${formatCurrency(s.totalAmount)}
474
+ `;
475
+ text2 += `Receipt Count: ${s.receiptCount}
476
+ `;
477
+ text2 += `Average: ${formatCurrency(s.averageAmount)}
478
+ `;
479
+ text2 += `Min: ${formatCurrency(s.minAmount)} | Max: ${formatCurrency(s.maxAmount)}`;
480
+ return text2;
481
+ }
482
+ if (!data.data?.length) return "No spending data found for this period.";
483
+ let text = `Spending by ${groupBy} (${data.period.from} to ${data.period.to})
484
+ `;
485
+ text += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
486
+ `;
487
+ if (groupBy === "vendor") {
488
+ data.data.forEach((item, i) => {
489
+ text += `${i + 1}. ${item.vendorName}: ${formatCurrency(item.totalAmount)} (${item.receiptCount} receipts)
490
+ `;
491
+ });
492
+ } else {
493
+ data.data.forEach((item) => {
494
+ text += `\u2022 ${item.period}: ${formatCurrency(item.totalAmount)} (${item.receiptCount} receipts)
495
+ `;
496
+ });
497
+ }
498
+ return text;
499
+ }
500
+ function formatTopVendors(vendors) {
501
+ if (!vendors?.length) return "No vendor data found.";
502
+ let text = `Top Vendors by Spending
503
+ `;
504
+ text += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
505
+ `;
506
+ vendors.forEach((v, i) => {
507
+ text += `${i + 1}. ${v.name}: ${formatCurrency(v.total)} (${v.count} receipts)
508
+ `;
509
+ });
510
+ return text;
511
+ }
512
+ function formatSpendingTrends(trends) {
513
+ if (!trends?.length) return "No trend data found.";
514
+ let text = `Monthly Spending Trends
515
+ `;
516
+ text += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
517
+ `;
518
+ trends.forEach((t) => {
519
+ const bar = "\u2588".repeat(Math.min(20, Math.round(t.total / 100)));
520
+ text += `${t.month}: ${formatCurrency(t.total)} ${bar}
521
+ `;
522
+ });
523
+ return text;
524
+ }
525
+ function formatUnmatchedSummary(data) {
526
+ let text = `Action Items Summary
527
+ `;
528
+ text += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
529
+
530
+ `;
531
+ text += `\u{1F4CB} Proposed Matches to Review: ${data.proposedMatches.count}
532
+ `;
533
+ text += `\u{1F4C4} Unmatched Receipts: ${data.unmatchedReceipts.count} (${formatCurrency(data.unmatchedReceipts.totalAmount)})
534
+ `;
535
+ text += `\u{1F4B3} Unmatched Transactions: ${data.unmatchedTransactions.count} (${formatCurrency(data.unmatchedTransactions.totalAmount)})
536
+ `;
537
+ text += `\u26A0\uFE0F Large Transactions (>${formatCurrency(data.largeUnmatchedTransactions.threshold)}) without receipts: ${data.largeUnmatchedTransactions.count}
538
+
539
+ `;
540
+ text += `${data.actionItems.message}`;
541
+ return text;
542
+ }
338
543
 
339
544
  // src/index.js
340
545
  var apiKey = process.env.MINTLINE_API_KEY;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mintline/mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "MCP server for Mintline - connect AI assistants to your receipts and transactions",
5
5
  "main": "dist/index.js",
6
6
  "bin": {