@warmio/mcp 1.0.3 → 1.2.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 (2) hide show
  1. package/dist/server.js +118 -67
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -11,6 +11,39 @@ import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import * as os from 'os';
13
13
  const API_URL = process.env.WARM_API_URL || 'https://warm.io';
14
+ const MAX_RESPONSE_SIZE = 50_000;
15
+ function compactTransaction(t) {
16
+ return {
17
+ d: t.date,
18
+ a: t.amount,
19
+ m: t.merchant_name || t.name || 'Unknown',
20
+ c: t.category || null,
21
+ };
22
+ }
23
+ function matchesSearch(t, search) {
24
+ const s = search.toLowerCase();
25
+ const merchant = (t.merchant_name || t.name || '').toLowerCase();
26
+ const category = (t.category || '').toLowerCase();
27
+ return merchant.includes(s) || category.includes(s);
28
+ }
29
+ function inDateRange(t, since, until) {
30
+ if (since && t.date < since)
31
+ return false;
32
+ if (until && t.date > until)
33
+ return false;
34
+ return true;
35
+ }
36
+ function calculateSummary(transactions) {
37
+ if (transactions.length === 0) {
38
+ return { total: 0, count: 0, avg: 0 };
39
+ }
40
+ const total = transactions.reduce((sum, t) => sum + t.a, 0);
41
+ return {
42
+ total: Math.round(total * 100) / 100,
43
+ count: transactions.length,
44
+ avg: Math.round((total / transactions.length) * 100) / 100,
45
+ };
46
+ }
14
47
  function getApiKey() {
15
48
  if (process.env.WARM_API_KEY) {
16
49
  return process.env.WARM_API_KEY;
@@ -49,85 +82,84 @@ async function apiRequest(endpoint, params = {}) {
49
82
  }
50
83
  return response.json();
51
84
  }
52
- const server = new Server({ name: 'warm', version: '1.0.3' }, { capabilities: { tools: {} } });
85
+ const server = new Server({ name: 'warm', version: '1.2.0' }, { capabilities: { tools: {} } });
53
86
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
54
87
  tools: [
55
88
  {
56
89
  name: 'get_accounts',
57
- description: 'Get all connected bank accounts with balances. Returns account names, types, balances, and institutions.',
90
+ description: 'Get all connected bank accounts with balances. Use for: "What accounts do I have?", "What is my checking balance?", "Show my credit cards". Returns: array of {name, type, balance, institution}.',
58
91
  inputSchema: {
59
92
  type: 'object',
60
- properties: {
61
- since: {
62
- type: 'string',
63
- description: 'Filter accounts updated since this date (ISO format)',
64
- },
65
- },
93
+ properties: {},
66
94
  },
95
+ annotations: { readOnlyHint: true },
67
96
  },
68
97
  {
69
98
  name: 'get_transactions',
70
- description: 'Get transactions from connected accounts. Supports pagination and date filtering.',
99
+ description: 'Search and analyze transactions. Use for: "How much did I spend on X?", "Show my Amazon purchases", "What did I buy last month?". Returns: {summary: {total, count, avg}, txns: [{d, a, m, c}]} where d=date, a=amount, m=merchant, c=category.',
71
100
  inputSchema: {
72
101
  type: 'object',
73
102
  properties: {
74
- limit: {
75
- type: 'number',
76
- description: 'Maximum number of transactions to return (default: 100)',
103
+ search: {
104
+ type: 'string',
105
+ description: 'Filter by merchant or category (e.g., "coffee", "amazon", "groceries")',
77
106
  },
78
107
  since: {
79
108
  type: 'string',
80
- description: 'Get transactions since this date (ISO format, e.g., 2024-01-01)',
109
+ description: 'Start date inclusive (YYYY-MM-DD)',
81
110
  },
82
- cursor: {
111
+ until: {
83
112
  type: 'string',
84
- description: 'Pagination cursor for fetching more results',
113
+ description: 'End date inclusive (YYYY-MM-DD)',
114
+ },
115
+ limit: {
116
+ type: 'number',
117
+ description: 'Max transactions (default: 50, max: 200)',
85
118
  },
86
119
  },
87
120
  },
121
+ annotations: { readOnlyHint: true },
88
122
  },
89
123
  {
90
124
  name: 'get_recurring',
91
- description: 'Get recurring payments and subscriptions detected from transaction history.',
125
+ description: 'Get detected subscriptions and recurring payments. Use for: "What subscriptions do I have?", "Show my monthly bills", "What are my recurring charges?". Returns: {recurring: [{merchant, amount, frequency, next_date}]}.',
92
126
  inputSchema: {
93
127
  type: 'object',
94
- properties: {
95
- since: {
96
- type: 'string',
97
- description: 'Filter recurring items detected since this date',
98
- },
99
- },
128
+ properties: {},
100
129
  },
130
+ annotations: { readOnlyHint: true },
101
131
  },
102
132
  {
103
133
  name: 'get_snapshots',
104
- description: 'Get net worth snapshots over time (daily or monthly aggregation).',
134
+ description: 'Get net worth history over time. Use for: "How has my net worth changed?", "Show my financial progress", "What was my balance last month?". Returns: {snapshots: [{d, nw, a, l}]} where nw=net_worth, a=assets, l=liabilities.',
105
135
  inputSchema: {
106
136
  type: 'object',
107
137
  properties: {
108
138
  granularity: {
109
139
  type: 'string',
110
140
  enum: ['daily', 'monthly'],
111
- description: 'Aggregation level (default: daily)',
141
+ description: 'daily or monthly (default: daily)',
112
142
  },
113
143
  limit: {
114
144
  type: 'number',
115
- description: 'Number of snapshots to return',
145
+ description: 'Number of snapshots (default: 30)',
116
146
  },
117
147
  since: {
118
148
  type: 'string',
119
- description: 'Start date for snapshots (ISO format)',
149
+ description: 'Start date (YYYY-MM-DD)',
120
150
  },
121
151
  },
122
152
  },
153
+ annotations: { readOnlyHint: true },
123
154
  },
124
155
  {
125
156
  name: 'verify_key',
126
- description: 'Check if the Warm API key is valid and working.',
157
+ description: 'Check if API key is valid and working.',
127
158
  inputSchema: {
128
159
  type: 'object',
129
160
  properties: {},
130
161
  },
162
+ annotations: { readOnlyHint: true },
131
163
  },
132
164
  ],
133
165
  }));
@@ -136,37 +168,62 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
136
168
  try {
137
169
  switch (name) {
138
170
  case 'get_accounts': {
139
- const params = {};
140
- if (args?.since)
141
- params.since = String(args.since);
142
- const data = await apiRequest('/api/accounts', params);
143
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
171
+ const data = await apiRequest('/api/accounts');
172
+ return { content: [{ type: 'text', text: JSON.stringify(data) }] };
144
173
  }
145
174
  case 'get_transactions': {
146
- const params = {};
147
- if (args?.limit)
148
- params.limit = String(args.limit);
149
- if (args?.since)
150
- params.last_knowledge = String(args.since);
151
- if (args?.cursor)
152
- params.cursor = String(args.cursor);
153
- const data = await apiRequest('/api/transactions', params);
154
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
175
+ const search = args?.search ? String(args.search) : undefined;
176
+ const since = args?.since ? String(args.since) : undefined;
177
+ const until = args?.until ? String(args.until) : undefined;
178
+ const requestedLimit = args?.limit ? Math.min(Number(args.limit), 200) : 50;
179
+ // Fetch more if filtering, since we'll reduce the set
180
+ const fetchLimit = search || until ? Math.min(requestedLimit * 10, 1000) : requestedLimit;
181
+ const params = { limit: String(fetchLimit) };
182
+ if (since)
183
+ params.last_knowledge = since;
184
+ const response = (await apiRequest('/api/transactions', params));
185
+ let transactions = (response.transactions || []);
186
+ // Apply date range filter (until is client-side since API only supports since)
187
+ if (until) {
188
+ transactions = transactions.filter((t) => inDateRange(t, since, until));
189
+ }
190
+ // Apply search filter
191
+ if (search) {
192
+ transactions = transactions.filter((t) => matchesSearch(t, search));
193
+ }
194
+ // Convert to compact format
195
+ const compactTxns = transactions.map(compactTransaction);
196
+ // Calculate summary on ALL matching transactions
197
+ const summary = calculateSummary(compactTxns);
198
+ // Apply limit for display
199
+ const limited = compactTxns.slice(0, requestedLimit);
200
+ const truncated = compactTxns.length > requestedLimit;
201
+ // Build compact result
202
+ const result = { summary, txns: limited };
203
+ if (truncated) {
204
+ result.more = compactTxns.length - requestedLimit;
205
+ }
206
+ // Size check and reduce if needed
207
+ let output = JSON.stringify(result);
208
+ if (output.length > MAX_RESPONSE_SIZE) {
209
+ const reducedCount = Math.floor(limited.length * (MAX_RESPONSE_SIZE / output.length) * 0.8);
210
+ result.txns = limited.slice(0, reducedCount);
211
+ result.more = compactTxns.length - reducedCount;
212
+ output = JSON.stringify(result);
213
+ }
214
+ return { content: [{ type: 'text', text: output }] };
155
215
  }
156
216
  case 'get_recurring': {
157
- const params = {};
158
- if (args?.since)
159
- params.since = String(args.since);
160
- const data = await apiRequest('/api/recurring', params);
161
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
217
+ const response = (await apiRequest('/api/transactions', { limit: '1' }));
218
+ const recurring = response.recurring || [];
219
+ return { content: [{ type: 'text', text: JSON.stringify({ recurring }) }] };
162
220
  }
163
221
  case 'get_snapshots': {
164
- const response = (await apiRequest('/api/transactions', {
165
- limit: '1',
166
- }));
222
+ const response = (await apiRequest('/api/transactions', { limit: '1' }));
167
223
  const snapshots = response.snapshots || [];
168
224
  const granularity = args?.granularity || 'daily';
169
- const limit = args?.limit ? Number(args.limit) : granularity === 'daily' ? 100 : 0;
225
+ const defaultLimit = granularity === 'daily' ? 30 : 0;
226
+ const limit = args?.limit ? Number(args.limit) : defaultLimit;
170
227
  const since = args?.since;
171
228
  let filtered = snapshots;
172
229
  if (since) {
@@ -176,8 +233,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
176
233
  const byMonth = new Map();
177
234
  filtered.forEach((s) => {
178
235
  const month = String(s.snapshot_date).substring(0, 7);
179
- if (!byMonth.has(month) ||
180
- String(s.snapshot_date) > String(byMonth.get(month).snapshot_date)) {
236
+ if (!byMonth.has(month) || String(s.snapshot_date) > String(byMonth.get(month).snapshot_date)) {
181
237
  byMonth.set(month, s);
182
238
  }
183
239
  });
@@ -187,23 +243,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
187
243
  if (limit > 0) {
188
244
  filtered = filtered.slice(0, limit);
189
245
  }
190
- const result = {
191
- granularity,
192
- snapshots: filtered.map((s) => ({
193
- date: s.snapshot_date,
194
- ...(granularity === 'monthly' && {
195
- month: String(s.snapshot_date).substring(0, 7),
196
- }),
197
- net_worth: s.net_worth,
198
- total_assets: s.total_assets,
199
- total_liabilities: s.total_liabilities,
200
- })),
201
- };
202
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
246
+ // Compact output
247
+ const result = filtered.map((s) => ({
248
+ d: s.snapshot_date,
249
+ nw: s.net_worth,
250
+ a: s.total_assets,
251
+ l: s.total_liabilities,
252
+ }));
253
+ return { content: [{ type: 'text', text: JSON.stringify({ granularity, snapshots: result }) }] };
203
254
  }
204
255
  case 'verify_key': {
205
256
  const data = await apiRequest('/api/verify');
206
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
257
+ return { content: [{ type: 'text', text: JSON.stringify(data) }] };
207
258
  }
208
259
  default:
209
260
  throw new Error(`Unknown tool: ${name}`);
@@ -212,7 +263,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
212
263
  catch (error) {
213
264
  const message = error instanceof Error ? error.message : String(error);
214
265
  return {
215
- content: [{ type: 'text', text: JSON.stringify({ error: message }, null, 2) }],
266
+ content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
216
267
  isError: true,
217
268
  };
218
269
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warmio/mcp",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for Warm Financial API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",