@warmio/mcp 2.1.0 → 2.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.
package/dist/api-types.js CHANGED
@@ -12,54 +12,22 @@ export function generateApiTypeString() {
12
12
  accounts: Array<{ name: string; type: string; balance: number; institution: string }>;
13
13
  }>;
14
14
 
15
- /** Get transactions. Amounts: positive = expense, negative = income. Category c: "INCOME"/"TRANSFER_IN" = income, others = expenses. */
15
+ /** Get transactions (up to 1000). Amounts: positive = expense, negative = income. Category c: "INCOME"/"TRANSFER_IN" = income, others = expenses. */
16
16
  getTransactions(params?: {
17
17
  since?: string; // YYYY-MM-DD inclusive
18
18
  until?: string; // YYYY-MM-DD inclusive
19
- limit?: number; // default: 200, max: 1000
20
19
  }): Promise<{
21
20
  summary: { total: number; count: number; avg: number };
22
21
  txns: Array<{ d: string; a: number; m: string; c: string | null }>;
23
22
  more?: number;
24
23
  }>;
25
24
 
26
- /** Get recurring payments and subscriptions. Amounts are positive. */
27
- getRecurring(): Promise<{
28
- recurring: Array<{ merchant: string; amount: number; frequency: string; next_date: string | null }>;
29
- }>;
30
-
31
- /** Get net worth history snapshots */
25
+ /** Get daily net worth history */
32
26
  getSnapshots(params?: {
33
- granularity?: "daily" | "monthly";
34
27
  limit?: number;
35
28
  since?: string;
36
29
  }): Promise<{
37
- granularity: string;
38
30
  snapshots: Array<{ d: string; nw: number; a: number; l: number }>;
39
31
  }>;
40
-
41
- /** Get budgets with spending progress */
42
- getBudgets(): Promise<{
43
- budgets: Array<{ name: string; amount: number; spent: number; remaining: number; percent_used: number; period: string; status: string }>;
44
- }>;
45
-
46
- /** Get savings goals with progress */
47
- getGoals(): Promise<{
48
- goals: Array<{ name: string; target: number; current: number; progress_percent: number; target_date: string | null; status: string }>;
49
- }>;
50
-
51
- /** Get financial health score (0-100) with pillar breakdown */
52
- getHealth(): Promise<{
53
- score: number | null;
54
- label: string | null;
55
- pillars: { spend: number; save: number; borrow: number; build: number } | null;
56
- data_completeness: number | null;
57
- }>;
58
-
59
- /** Get spending breakdown by category. Amounts are positive. */
60
- getSpending(params?: { months?: number }): Promise<{
61
- spending: Array<{ category: string; total: number; count: number }>;
62
- period: { start: string; end: string };
63
- }>;
64
32
  };`;
65
33
  }
package/dist/sandbox.js CHANGED
@@ -53,12 +53,7 @@ async function __run() {
53
53
  const warm = {
54
54
  getAccounts: () => __callApi('get_accounts', '{}').then(JSON.parse),
55
55
  getTransactions: (p) => __callApi('get_transactions', JSON.stringify(p || {})).then(JSON.parse),
56
- getRecurring: () => __callApi('get_recurring', '{}').then(JSON.parse),
57
56
  getSnapshots: (p) => __callApi('get_snapshots', JSON.stringify(p || {})).then(JSON.parse),
58
- getBudgets: () => __callApi('get_budgets', '{}').then(JSON.parse),
59
- getGoals: () => __callApi('get_goals', '{}').then(JSON.parse),
60
- getHealth: () => __callApi('get_health', '{}').then(JSON.parse),
61
- getSpending: (p) => __callApi('get_spending', JSON.stringify(p || {})).then(JSON.parse),
62
57
  };
63
58
  ${code}
64
59
  }
package/dist/server.js CHANGED
@@ -113,20 +113,23 @@ async function apiRequest(endpoint, params = {}) {
113
113
  403: 'Pro subscription required. Upgrade at https://warm.io/settings',
114
114
  429: 'Rate limit exceeded. Try again in a few minutes.',
115
115
  };
116
- throw new Error(errorMessages[response.status] || `HTTP ${response.status}`);
116
+ if (errorMessages[response.status]) {
117
+ throw new Error(errorMessages[response.status]);
118
+ }
119
+ // Read the actual error message from the API response body
120
+ let detail = `HTTP ${response.status}`;
121
+ try {
122
+ const body = (await response.json());
123
+ if (body?.error)
124
+ detail = body.error;
125
+ }
126
+ catch { /* ignore parse failures */ }
127
+ throw new Error(detail);
117
128
  }
118
129
  return response.json();
119
130
  }
120
- function sizeCheck(data, maxSize) {
121
- let output = JSON.stringify(data);
122
- if (output.length > maxSize) {
123
- const reducedCount = Math.floor(data.length * (maxSize / output.length) * 0.8);
124
- return data.slice(0, reducedCount);
125
- }
126
- return data;
127
- }
128
131
  // ============================================
129
- // EXTRACTED TOOL HANDLERS
132
+ // TOOL HANDLERS
130
133
  // ============================================
131
134
  async function handleGetAccounts() {
132
135
  const response = (await apiRequest('/api/accounts'));
@@ -137,10 +140,6 @@ async function handleGetAccounts() {
137
140
  async function handleGetTransactions(args) {
138
141
  const since = args?.since ? String(args.since) : undefined;
139
142
  const until = args?.until ? String(args.until) : undefined;
140
- const parsedLimit = args?.limit ? Number(args.limit) : 200;
141
- const requestedLimit = Number.isFinite(parsedLimit)
142
- ? Math.max(1, Math.min(Math.floor(parsedLimit), 1000))
143
- : 200;
144
143
  let transactions = [];
145
144
  let cursor;
146
145
  let pagesFetched = 0;
@@ -149,7 +148,8 @@ async function handleGetTransactions(args) {
149
148
  const params = {
150
149
  limit: String(TRANSACTION_PAGE_SIZE),
151
150
  };
152
- if (since)
151
+ // API rejects last_knowledge + cursor together; only use last_knowledge on first page
152
+ if (since && !cursor)
153
153
  params.last_knowledge = since;
154
154
  if (cursor)
155
155
  params.cursor = cursor;
@@ -165,11 +165,11 @@ async function handleGetTransactions(args) {
165
165
  }
166
166
  const compactTxns = transactions.map(compactTransaction);
167
167
  const summary = calculateSummary(compactTxns);
168
- const limited = compactTxns.slice(0, requestedLimit);
169
- const truncated = compactTxns.length > requestedLimit;
168
+ const limited = compactTxns.slice(0, 1000);
169
+ const truncated = compactTxns.length > 1000;
170
170
  const result = { summary, txns: limited };
171
171
  if (truncated) {
172
- result.more = compactTxns.length - requestedLimit;
172
+ result.more = compactTxns.length - 1000;
173
173
  }
174
174
  let output = JSON.stringify(result);
175
175
  if (output.length > MAX_RESPONSE_SIZE) {
@@ -179,29 +179,10 @@ async function handleGetTransactions(args) {
179
179
  }
180
180
  return result;
181
181
  }
182
- async function handleGetRecurring() {
183
- const response = (await apiRequest('/api/subscriptions'));
184
- const raw = response.recurring_transactions || [];
185
- const recurring = raw.map((r) => ({
186
- merchant: String(r.merchant_name || r.merchant || r.name || 'Unknown'),
187
- // Normalize to positive amounts
188
- amount: Math.round(Math.abs(Number(r.amount) || 0) * 100) / 100,
189
- frequency: String(r.frequency || ''),
190
- next_date: r.next_date ?? null,
191
- }));
192
- const checked = sizeCheck(recurring, MAX_RESPONSE_SIZE);
193
- const result = { recurring: checked };
194
- if (checked.length < recurring.length) {
195
- result.more = recurring.length - checked.length;
196
- }
197
- return result;
198
- }
199
182
  async function handleGetSnapshots(args) {
200
183
  const response = (await apiRequest('/api/snapshots'));
201
184
  const snapshots = response.snapshots || [];
202
- const granularity = args?.granularity || 'daily';
203
- const defaultLimit = granularity === 'daily' ? 30 : 0;
204
- const limit = args?.limit ? Number(args.limit) : defaultLimit;
185
+ const limit = args?.limit ? Number(args.limit) : 30;
205
186
  const since = args?.since;
206
187
  // Normalize snapshot dates (support both snapshot_date and d)
207
188
  const normalized = snapshots.map((s) => ({
@@ -214,16 +195,6 @@ async function handleGetSnapshots(args) {
214
195
  if (since) {
215
196
  filtered = filtered.filter((s) => s.date >= since);
216
197
  }
217
- if (granularity === 'monthly') {
218
- const byMonth = new Map();
219
- filtered.forEach((s) => {
220
- const month = s.date.substring(0, 7);
221
- if (!byMonth.has(month) || s.date > (byMonth.get(month)?.date || '')) {
222
- byMonth.set(month, s);
223
- }
224
- });
225
- filtered = Array.from(byMonth.values());
226
- }
227
198
  filtered.sort((a, b) => b.date.localeCompare(a.date));
228
199
  if (limit > 0) {
229
200
  filtered = filtered.slice(0, limit);
@@ -234,7 +205,7 @@ async function handleGetSnapshots(args) {
234
205
  a: Math.round(s.total_assets * 100) / 100,
235
206
  l: Math.round(s.total_liabilities * 100) / 100,
236
207
  }));
237
- return { granularity, snapshots: result };
208
+ return { snapshots: result };
238
209
  }
239
210
  async function handleVerifyKey() {
240
211
  const response = (await apiRequest('/api/verify'));
@@ -243,72 +214,12 @@ async function handleVerifyKey() {
243
214
  status: response.status || (response.valid ? 'ok' : 'invalid'),
244
215
  };
245
216
  }
246
- async function handleGetBudgets() {
247
- const response = (await apiRequest('/api/budgets'));
248
- // Filter to only include spec-defined fields
249
- const budgets = (response.budgets || []).map((b) => ({
250
- name: String(b.name || ''),
251
- amount: Number(b.amount || 0),
252
- spent: Number(b.spent || 0),
253
- remaining: Number(b.remaining || 0),
254
- percent_used: Number(b.percent_used || 0),
255
- period: String(b.period || ''),
256
- status: String(b.status || ''),
257
- }));
258
- return { budgets };
259
- }
260
- async function handleGetGoals() {
261
- const response = (await apiRequest('/api/goals'));
262
- // Filter to only include spec-defined fields
263
- const goals = (response.goals || []).map((g) => ({
264
- name: String(g.name || ''),
265
- target: Number(g.target || 0),
266
- current: Number(g.current || 0),
267
- progress_percent: Number(g.progress_percent || 0),
268
- target_date: g.target_date ?? null,
269
- status: String(g.status || ''),
270
- }));
271
- return { goals };
272
- }
273
- async function handleGetHealth() {
274
- const response = (await apiRequest('/api/health'));
275
- return {
276
- score: response.score ?? null,
277
- label: response.label ?? null,
278
- pillars: response.pillars
279
- ? {
280
- spend: Number(response.pillars.spend || 0),
281
- save: Number(response.pillars.save || 0),
282
- borrow: Number(response.pillars.borrow || 0),
283
- build: Number(response.pillars.build || 0),
284
- }
285
- : null,
286
- data_completeness: response.data_completeness ?? null,
287
- };
288
- }
289
- async function handleGetSpending(args) {
290
- const months = args?.months ? String(args.months) : '6';
291
- const response = (await apiRequest('/api/spending', { months }));
292
- return {
293
- spending: (response.spending || []).map((s) => ({
294
- category: String(s.category || ''),
295
- total: Math.round(Number(s.total || 0) * 100) / 100,
296
- count: Number(s.count || 0),
297
- })),
298
- period: response.period || { start: '', end: '' },
299
- };
300
- }
301
217
  // Tool name → handler mapping for sandbox dispatch
302
218
  const toolHandlers = {
303
219
  get_accounts: handleGetAccounts,
304
220
  get_transactions: handleGetTransactions,
305
- get_recurring: handleGetRecurring,
306
221
  get_snapshots: handleGetSnapshots,
307
222
  verify_key: handleVerifyKey,
308
- get_budgets: handleGetBudgets,
309
- get_goals: handleGetGoals,
310
- get_health: handleGetHealth,
311
- get_spending: handleGetSpending,
312
223
  };
313
224
  // ============================================
314
225
  // SERVER SETUP
@@ -327,49 +238,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
327
238
  },
328
239
  {
329
240
  name: 'get_transactions',
330
- description: 'Get transactions and analyze spending. Use for: "How much did I spend on coffee?", "Show my purchases", "What did I buy last month?", "Show my income". Use the `c` (category) field to filter: INCOME and TRANSFER_IN = income, all others = expenses. Amounts: positive = expense, negative = income/deposit.\nReturns: { summary: { total: number; count: number; avg: number }; txns: Array<{ d: string; a: number; m: string; c: string | null }>; more?: number }',
241
+ description: 'Get transactions (up to 1000). This is the PRIMARY tool for all spending, income, and merchant questions. Filter results by merchant name `m` or category `c` to answer specific questions. Categories in `c`: INCOME and TRANSFER_IN = income, all others = expenses. Amounts: positive = expense, negative = income/deposit. Call with NO parameters to get all recent transactions.\nReturns: { summary: { total: number; count: number; avg: number }; txns: Array<{ d: string; a: number; m: string; c: string | null }>; more?: number }',
331
242
  inputSchema: {
332
243
  type: 'object',
333
244
  properties: {
334
245
  since: {
335
246
  type: 'string',
336
- description: 'Start date inclusive (YYYY-MM-DD)',
247
+ description: 'Start date inclusive (YYYY-MM-DD). Omit to get all available transactions.',
337
248
  },
338
249
  until: {
339
250
  type: 'string',
340
- description: 'End date inclusive (YYYY-MM-DD)',
341
- },
342
- limit: {
343
- type: 'number',
344
- description: 'Max transactions to return (default: 200, max: 1000)',
251
+ description: 'End date inclusive (YYYY-MM-DD). Omit for no end date filter.',
345
252
  },
346
253
  },
347
254
  },
348
255
  annotations: { readOnlyHint: true },
349
256
  },
350
- {
351
- name: 'get_recurring',
352
- description: 'Get detected subscriptions and recurring payments. Use for: "What subscriptions do I have?", "Show my monthly bills", "What are my recurring charges?".\nReturns: { recurring: Array<{ merchant: string; amount: number; frequency: string; next_date: string | null }> }',
353
- inputSchema: {
354
- type: 'object',
355
- properties: {},
356
- },
357
- annotations: { readOnlyHint: true },
358
- },
359
257
  {
360
258
  name: 'get_snapshots',
361
- 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?".\nReturns: { granularity: string; snapshots: Array<{ d: string; nw: number; a: number; l: number }> }',
259
+ description: 'Get daily net worth history. Use for: "How has my net worth changed?", "Show my financial progress", "What was my net worth last month?".\nReturns: { snapshots: Array<{ d: string; nw: number; a: number; l: number }> }',
362
260
  inputSchema: {
363
261
  type: 'object',
364
262
  properties: {
365
- granularity: {
366
- type: 'string',
367
- enum: ['daily', 'monthly'],
368
- description: 'daily or monthly (default: daily)',
369
- },
370
263
  limit: {
371
264
  type: 'number',
372
- description: 'Number of snapshots (default: 30)',
265
+ description: 'Number of daily snapshots to return (default: 30)',
373
266
  },
374
267
  since: {
375
268
  type: 'string',
@@ -379,47 +272,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
379
272
  },
380
273
  annotations: { readOnlyHint: true },
381
274
  },
382
- {
383
- name: 'get_budgets',
384
- description: 'Get all budgets with current spending progress. Use for: "How are my budgets?", "Am I over budget?", "Show my budget status".\nReturns: { budgets: Array<{ name: string; amount: number; spent: number; remaining: number; percent_used: number; period: string; status: string }> }',
385
- inputSchema: {
386
- type: 'object',
387
- properties: {},
388
- },
389
- annotations: { readOnlyHint: true },
390
- },
391
- {
392
- name: 'get_goals',
393
- description: 'Get savings goals with progress. Use for: "How are my goals?", "Savings progress", "Am I on track for my goals?".\nReturns: { goals: Array<{ name: string; target: number; current: number; progress_percent: number; target_date: string | null; status: string }> }',
394
- inputSchema: {
395
- type: 'object',
396
- properties: {},
397
- },
398
- annotations: { readOnlyHint: true },
399
- },
400
- {
401
- name: 'get_health',
402
- description: 'Get financial health score and pillar breakdown. Use for: "What\'s my financial health?", "How am I doing financially?", "Health score".\nReturns: { score: number | null; label: string | null; pillars: { spend: number; save: number; borrow: number; build: number } | null; data_completeness: number | null }',
403
- inputSchema: {
404
- type: 'object',
405
- properties: {},
406
- },
407
- annotations: { readOnlyHint: true },
408
- },
409
- {
410
- name: 'get_spending',
411
- description: 'Get spending breakdown by category over a period. Use for: "Where does my money go?", "Spending by category", "Top spending categories".\nReturns: { spending: Array<{ category: string; total: number; count: number }>; period: { start: string; end: string } }',
412
- inputSchema: {
413
- type: 'object',
414
- properties: {
415
- months: {
416
- type: 'number',
417
- description: 'Number of months to analyze (default: 6, max: 24)',
418
- },
419
- },
420
- },
421
- annotations: { readOnlyHint: true },
422
- },
423
275
  {
424
276
  name: 'run_analysis',
425
277
  description: `Run JavaScript code that calls warm.* functions for complex multi-step analysis. Use when a query requires combining data from multiple tools, custom calculations, or comparisons that would take 3+ tool calls.\n\nAvailable API:\n${generateApiTypeString()}\n\nUse console.log() to output results. Example:\nconst [accounts, txns] = await Promise.all([warm.getAccounts(), warm.getTransactions({ since: "2024-01-01" })]);\nconst total = txns.txns.reduce((s, t) => s + t.a, 0);\nconsole.log(JSON.stringify({ accounts: accounts.accounts.length, totalSpent: total }));`,
@@ -437,7 +289,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
437
289
  },
438
290
  {
439
291
  name: 'verify_key',
440
- description: 'Check if API key is valid and working.\nReturns: { valid: boolean; user_id: string }',
292
+ description: 'Check if API key is valid and working.\nReturns: { valid: boolean; status: string }',
441
293
  inputSchema: {
442
294
  type: 'object',
443
295
  properties: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warmio/mcp",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "MCP server for Warm Financial API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",