@warmio/mcp 2.1.0 → 3.0.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.
- package/dist/server.d.ts +3 -0
- package/dist/server.js +42 -229
- package/package.json +2 -3
- package/dist/api-types.d.ts +0 -8
- package/dist/api-types.js +0 -65
- package/dist/sandbox.d.ts +0 -10
- package/dist/sandbox.js +0 -92
package/dist/server.d.ts
CHANGED
|
@@ -3,5 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides financial data from the Warm API as MCP tools.
|
|
5
5
|
* Reads API key from WARM_API_KEY env var or ~/.config/warm/api_key.
|
|
6
|
+
*
|
|
7
|
+
* Four read-only tools: get_accounts, get_transactions, get_snapshots, verify_key.
|
|
8
|
+
* The AI client handles all analysis — no sandbox needed.
|
|
6
9
|
*/
|
|
7
10
|
export {};
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides financial data from the Warm API as MCP tools.
|
|
5
5
|
* Reads API key from WARM_API_KEY env var or ~/.config/warm/api_key.
|
|
6
|
+
*
|
|
7
|
+
* Four read-only tools: get_accounts, get_transactions, get_snapshots, verify_key.
|
|
8
|
+
* The AI client handles all analysis — no sandbox needed.
|
|
6
9
|
*/
|
|
7
10
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
8
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -10,10 +13,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
10
13
|
import * as fs from 'fs';
|
|
11
14
|
import * as path from 'path';
|
|
12
15
|
import * as os from 'os';
|
|
13
|
-
import { generateApiTypeString } from './api-types.js';
|
|
14
|
-
import { executeSandboxedCode } from './sandbox.js';
|
|
15
16
|
const API_URL = process.env.WARM_API_URL || 'https://warm.io';
|
|
16
|
-
const MAX_RESPONSE_SIZE = 50_000;
|
|
17
17
|
const MAX_TRANSACTION_PAGES = 10;
|
|
18
18
|
const MAX_TRANSACTION_SCAN = 5_000;
|
|
19
19
|
const TRANSACTION_PAGE_SIZE = 200;
|
|
@@ -25,10 +25,8 @@ let cachedApiKey;
|
|
|
25
25
|
function compactTransaction(t) {
|
|
26
26
|
return {
|
|
27
27
|
d: t.date || '',
|
|
28
|
-
// Positive = expense, negative = income/deposit (Plaid convention)
|
|
29
28
|
a: t.amount ? Math.round(t.amount * 100) / 100 : 0,
|
|
30
29
|
m: t.merchant_name || t.name || 'Unknown',
|
|
31
|
-
// Include category (null if not set)
|
|
32
30
|
c: t.primary_category ?? null,
|
|
33
31
|
};
|
|
34
32
|
}
|
|
@@ -113,20 +111,22 @@ async function apiRequest(endpoint, params = {}) {
|
|
|
113
111
|
403: 'Pro subscription required. Upgrade at https://warm.io/settings',
|
|
114
112
|
429: 'Rate limit exceeded. Try again in a few minutes.',
|
|
115
113
|
};
|
|
116
|
-
|
|
114
|
+
if (errorMessages[response.status]) {
|
|
115
|
+
throw new Error(errorMessages[response.status]);
|
|
116
|
+
}
|
|
117
|
+
let detail = `HTTP ${response.status}`;
|
|
118
|
+
try {
|
|
119
|
+
const body = (await response.json());
|
|
120
|
+
if (body?.error)
|
|
121
|
+
detail = body.error;
|
|
122
|
+
}
|
|
123
|
+
catch { /* ignore parse failures */ }
|
|
124
|
+
throw new Error(detail);
|
|
117
125
|
}
|
|
118
126
|
return response.json();
|
|
119
127
|
}
|
|
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
128
|
// ============================================
|
|
129
|
-
//
|
|
129
|
+
// TOOL HANDLERS
|
|
130
130
|
// ============================================
|
|
131
131
|
async function handleGetAccounts() {
|
|
132
132
|
const response = (await apiRequest('/api/accounts'));
|
|
@@ -137,10 +137,8 @@ async function handleGetAccounts() {
|
|
|
137
137
|
async function handleGetTransactions(args) {
|
|
138
138
|
const since = args?.since ? String(args.since) : undefined;
|
|
139
139
|
const until = args?.until ? String(args.until) : undefined;
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
? Math.max(1, Math.min(Math.floor(parsedLimit), 1000))
|
|
143
|
-
: 200;
|
|
140
|
+
const limit = Math.min(Math.max(args?.limit ? Number(args.limit) : 200, 1), 500);
|
|
141
|
+
const offset = Math.max(args?.offset ? Number(args.offset) : 0, 0);
|
|
144
142
|
let transactions = [];
|
|
145
143
|
let cursor;
|
|
146
144
|
let pagesFetched = 0;
|
|
@@ -149,7 +147,7 @@ async function handleGetTransactions(args) {
|
|
|
149
147
|
const params = {
|
|
150
148
|
limit: String(TRANSACTION_PAGE_SIZE),
|
|
151
149
|
};
|
|
152
|
-
if (since)
|
|
150
|
+
if (since && !cursor)
|
|
153
151
|
params.last_knowledge = since;
|
|
154
152
|
if (cursor)
|
|
155
153
|
params.cursor = cursor;
|
|
@@ -165,45 +163,20 @@ async function handleGetTransactions(args) {
|
|
|
165
163
|
}
|
|
166
164
|
const compactTxns = transactions.map(compactTransaction);
|
|
167
165
|
const summary = calculateSummary(compactTxns);
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const reducedCount = Math.floor(limited.length * (MAX_RESPONSE_SIZE / output.length) * 0.8);
|
|
177
|
-
result.txns = limited.slice(0, reducedCount);
|
|
178
|
-
result.more = compactTxns.length - reducedCount;
|
|
179
|
-
}
|
|
180
|
-
return result;
|
|
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;
|
|
166
|
+
const total = compactTxns.length;
|
|
167
|
+
const page = compactTxns.slice(offset, offset + limit);
|
|
168
|
+
return {
|
|
169
|
+
summary,
|
|
170
|
+
txns: page,
|
|
171
|
+
total,
|
|
172
|
+
has_more: offset + limit < total,
|
|
173
|
+
};
|
|
198
174
|
}
|
|
199
175
|
async function handleGetSnapshots(args) {
|
|
200
176
|
const response = (await apiRequest('/api/snapshots'));
|
|
201
177
|
const snapshots = response.snapshots || [];
|
|
202
|
-
const
|
|
203
|
-
const defaultLimit = granularity === 'daily' ? 30 : 0;
|
|
204
|
-
const limit = args?.limit ? Number(args.limit) : defaultLimit;
|
|
178
|
+
const limit = args?.limit ? Number(args.limit) : 30;
|
|
205
179
|
const since = args?.since;
|
|
206
|
-
// Normalize snapshot dates (support both snapshot_date and d)
|
|
207
180
|
const normalized = snapshots.map((s) => ({
|
|
208
181
|
date: s.snapshot_date || s.d || '',
|
|
209
182
|
net_worth: s.net_worth ?? s.nw ?? 0,
|
|
@@ -214,16 +187,6 @@ async function handleGetSnapshots(args) {
|
|
|
214
187
|
if (since) {
|
|
215
188
|
filtered = filtered.filter((s) => s.date >= since);
|
|
216
189
|
}
|
|
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
190
|
filtered.sort((a, b) => b.date.localeCompare(a.date));
|
|
228
191
|
if (limit > 0) {
|
|
229
192
|
filtered = filtered.slice(0, limit);
|
|
@@ -234,7 +197,7 @@ async function handleGetSnapshots(args) {
|
|
|
234
197
|
a: Math.round(s.total_assets * 100) / 100,
|
|
235
198
|
l: Math.round(s.total_liabilities * 100) / 100,
|
|
236
199
|
}));
|
|
237
|
-
return {
|
|
200
|
+
return { snapshots: result };
|
|
238
201
|
}
|
|
239
202
|
async function handleVerifyKey() {
|
|
240
203
|
const response = (await apiRequest('/api/verify'));
|
|
@@ -243,82 +206,21 @@ async function handleVerifyKey() {
|
|
|
243
206
|
status: response.status || (response.valid ? 'ok' : 'invalid'),
|
|
244
207
|
};
|
|
245
208
|
}
|
|
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
|
-
// Tool name → handler mapping for sandbox dispatch
|
|
302
209
|
const toolHandlers = {
|
|
303
210
|
get_accounts: handleGetAccounts,
|
|
304
211
|
get_transactions: handleGetTransactions,
|
|
305
|
-
get_recurring: handleGetRecurring,
|
|
306
212
|
get_snapshots: handleGetSnapshots,
|
|
307
213
|
verify_key: handleVerifyKey,
|
|
308
|
-
get_budgets: handleGetBudgets,
|
|
309
|
-
get_goals: handleGetGoals,
|
|
310
|
-
get_health: handleGetHealth,
|
|
311
|
-
get_spending: handleGetSpending,
|
|
312
214
|
};
|
|
313
215
|
// ============================================
|
|
314
216
|
// SERVER SETUP
|
|
315
217
|
// ============================================
|
|
316
|
-
const server = new Server({ name: 'warm', version: '
|
|
218
|
+
const server = new Server({ name: 'warm', version: '3.0.0' }, { capabilities: { tools: {} } });
|
|
317
219
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
318
220
|
tools: [
|
|
319
221
|
{
|
|
320
222
|
name: 'get_accounts',
|
|
321
|
-
description: 'Get all connected bank accounts with balances
|
|
223
|
+
description: 'Get all connected bank accounts with current balances.\n\nReturns: { accounts: Array<{ name: string; type: string; balance: number; institution: string }> }\n\nAccount types: depository (checking/savings), credit, loan, investment, other.',
|
|
322
224
|
inputSchema: {
|
|
323
225
|
type: 'object',
|
|
324
226
|
properties: {},
|
|
@@ -327,117 +229,51 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
327
229
|
},
|
|
328
230
|
{
|
|
329
231
|
name: 'get_transactions',
|
|
330
|
-
description: 'Get transactions and
|
|
232
|
+
description: 'Get transactions with date filtering and pagination. Returns a summary of the FULL date range plus a paginated slice of individual transactions.\n\nAmounts: positive = expense/debit, negative = income/credit (Plaid convention).\nCategories in field `c`: INCOME, TRANSFER_IN = income. FOOD_AND_DRINK, TRANSPORTATION, ENTERTAINMENT, GENERAL_MERCHANDISE, RENT_AND_UTILITIES, LOAN_PAYMENTS, etc. = expenses.\n\nReturns: { summary: { total: number; count: number; avg: number }; txns: Array<{ d: string; a: number; m: string; c: string | null }>; total: number; has_more: boolean }\n\nCall multiple times with increasing offset to paginate. The summary is always computed over ALL matching transactions regardless of limit/offset.',
|
|
331
233
|
inputSchema: {
|
|
332
234
|
type: 'object',
|
|
333
235
|
properties: {
|
|
334
236
|
since: {
|
|
335
237
|
type: 'string',
|
|
336
|
-
description: 'Start date inclusive (YYYY-MM-DD)',
|
|
238
|
+
description: 'Start date inclusive (YYYY-MM-DD). Omit to get all available transactions.',
|
|
337
239
|
},
|
|
338
240
|
until: {
|
|
339
241
|
type: 'string',
|
|
340
|
-
description: 'End date inclusive (YYYY-MM-DD)',
|
|
242
|
+
description: 'End date inclusive (YYYY-MM-DD). Omit for no end date filter.',
|
|
341
243
|
},
|
|
342
244
|
limit: {
|
|
343
245
|
type: 'number',
|
|
344
|
-
description: 'Max transactions
|
|
246
|
+
description: 'Max transactions per page (default 200, max 500).',
|
|
247
|
+
},
|
|
248
|
+
offset: {
|
|
249
|
+
type: 'number',
|
|
250
|
+
description: 'Skip N transactions for pagination (default 0).',
|
|
345
251
|
},
|
|
346
252
|
},
|
|
347
253
|
},
|
|
348
254
|
annotations: { readOnlyHint: true },
|
|
349
255
|
},
|
|
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
256
|
{
|
|
360
257
|
name: 'get_snapshots',
|
|
361
|
-
description: 'Get net worth
|
|
258
|
+
description: 'Get daily net worth snapshots over time.\n\nReturns: { snapshots: Array<{ d: string; nw: number; a: number; l: number }> }\n\nFields: d = date, nw = net worth, a = total assets, l = total liabilities. Sorted newest first.',
|
|
362
259
|
inputSchema: {
|
|
363
260
|
type: 'object',
|
|
364
261
|
properties: {
|
|
365
|
-
granularity: {
|
|
366
|
-
type: 'string',
|
|
367
|
-
enum: ['daily', 'monthly'],
|
|
368
|
-
description: 'daily or monthly (default: daily)',
|
|
369
|
-
},
|
|
370
262
|
limit: {
|
|
371
263
|
type: 'number',
|
|
372
|
-
description: '
|
|
264
|
+
description: 'Max snapshots to return (default 30).',
|
|
373
265
|
},
|
|
374
266
|
since: {
|
|
375
267
|
type: 'string',
|
|
376
|
-
description: 'Start date (YYYY-MM-DD)',
|
|
268
|
+
description: 'Start date inclusive (YYYY-MM-DD).',
|
|
377
269
|
},
|
|
378
270
|
},
|
|
379
271
|
},
|
|
380
272
|
annotations: { readOnlyHint: true },
|
|
381
273
|
},
|
|
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
|
-
{
|
|
424
|
-
name: 'run_analysis',
|
|
425
|
-
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 }));`,
|
|
426
|
-
inputSchema: {
|
|
427
|
-
type: 'object',
|
|
428
|
-
properties: {
|
|
429
|
-
code: {
|
|
430
|
-
type: 'string',
|
|
431
|
-
description: 'JavaScript code to execute. Use warm.* functions and console.log() for output.',
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
required: ['code'],
|
|
435
|
-
},
|
|
436
|
-
annotations: { readOnlyHint: true },
|
|
437
|
-
},
|
|
438
274
|
{
|
|
439
275
|
name: 'verify_key',
|
|
440
|
-
description: 'Check if API key is valid
|
|
276
|
+
description: 'Check if the API key is valid.\n\nReturns: { valid: boolean; status: string }',
|
|
441
277
|
inputSchema: {
|
|
442
278
|
type: 'object',
|
|
443
279
|
properties: {},
|
|
@@ -449,29 +285,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
449
285
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
450
286
|
const { name, arguments: args } = request.params;
|
|
451
287
|
try {
|
|
452
|
-
// Handle run_analysis separately (sandbox execution)
|
|
453
|
-
if (name === 'run_analysis') {
|
|
454
|
-
const code = args?.code ? String(args.code) : '';
|
|
455
|
-
if (!code) {
|
|
456
|
-
throw new Error('Code parameter is required');
|
|
457
|
-
}
|
|
458
|
-
const callApi = async (tool, params) => {
|
|
459
|
-
const handler = toolHandlers[tool];
|
|
460
|
-
if (!handler) {
|
|
461
|
-
throw new Error(`Unknown tool: ${tool}`);
|
|
462
|
-
}
|
|
463
|
-
return handler(params);
|
|
464
|
-
};
|
|
465
|
-
const result = await executeSandboxedCode(code, callApi);
|
|
466
|
-
const text = result.error
|
|
467
|
-
? `Output:\n${result.output}\n\nError: ${result.error}`
|
|
468
|
-
: result.output;
|
|
469
|
-
return {
|
|
470
|
-
content: [{ type: 'text', text }],
|
|
471
|
-
isError: !!result.error,
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
// Standard tool dispatch
|
|
475
288
|
const handler = toolHandlers[name];
|
|
476
289
|
if (!handler) {
|
|
477
290
|
throw new Error(`Unknown tool: ${name}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warmio/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "MCP server for Warm Financial API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,8 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/sdk": "^1.25.0"
|
|
27
|
-
"quickjs-emscripten": "^0.31.0"
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.25.0"
|
|
28
27
|
},
|
|
29
28
|
"devDependencies": {
|
|
30
29
|
"@types/node": "^22.0.0",
|
package/dist/api-types.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TypeScript API type definitions for MCP code mode.
|
|
3
|
-
* Embedded in the run_analysis tool description so the LLM
|
|
4
|
-
* knows the shape of each warm.* function.
|
|
5
|
-
*
|
|
6
|
-
* All monetary amounts are positive (normalized).
|
|
7
|
-
*/
|
|
8
|
-
export declare function generateApiTypeString(): string;
|
package/dist/api-types.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TypeScript API type definitions for MCP code mode.
|
|
3
|
-
* Embedded in the run_analysis tool description so the LLM
|
|
4
|
-
* knows the shape of each warm.* function.
|
|
5
|
-
*
|
|
6
|
-
* All monetary amounts are positive (normalized).
|
|
7
|
-
*/
|
|
8
|
-
export function generateApiTypeString() {
|
|
9
|
-
return `declare const warm: {
|
|
10
|
-
/** Get all connected bank accounts with balances */
|
|
11
|
-
getAccounts(): Promise<{
|
|
12
|
-
accounts: Array<{ name: string; type: string; balance: number; institution: string }>;
|
|
13
|
-
}>;
|
|
14
|
-
|
|
15
|
-
/** Get transactions. Amounts: positive = expense, negative = income. Category c: "INCOME"/"TRANSFER_IN" = income, others = expenses. */
|
|
16
|
-
getTransactions(params?: {
|
|
17
|
-
since?: string; // YYYY-MM-DD inclusive
|
|
18
|
-
until?: string; // YYYY-MM-DD inclusive
|
|
19
|
-
limit?: number; // default: 200, max: 1000
|
|
20
|
-
}): Promise<{
|
|
21
|
-
summary: { total: number; count: number; avg: number };
|
|
22
|
-
txns: Array<{ d: string; a: number; m: string; c: string | null }>;
|
|
23
|
-
more?: number;
|
|
24
|
-
}>;
|
|
25
|
-
|
|
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 */
|
|
32
|
-
getSnapshots(params?: {
|
|
33
|
-
granularity?: "daily" | "monthly";
|
|
34
|
-
limit?: number;
|
|
35
|
-
since?: string;
|
|
36
|
-
}): Promise<{
|
|
37
|
-
granularity: string;
|
|
38
|
-
snapshots: Array<{ d: string; nw: number; a: number; l: number }>;
|
|
39
|
-
}>;
|
|
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
|
-
};`;
|
|
65
|
-
}
|
package/dist/sandbox.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QuickJS Sandbox for MCP Code Mode
|
|
3
|
-
*
|
|
4
|
-
* Executes user-provided JavaScript code in a sandboxed QuickJS WASM runtime.
|
|
5
|
-
* Injects `warm.*` functions that delegate to a `callApi` callback.
|
|
6
|
-
*/
|
|
7
|
-
export declare function executeSandboxedCode(code: string, callApi: (tool: string, params: Record<string, unknown>) => Promise<unknown>): Promise<{
|
|
8
|
-
output: string;
|
|
9
|
-
error?: string;
|
|
10
|
-
}>;
|
package/dist/sandbox.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QuickJS Sandbox for MCP Code Mode
|
|
3
|
-
*
|
|
4
|
-
* Executes user-provided JavaScript code in a sandboxed QuickJS WASM runtime.
|
|
5
|
-
* Injects `warm.*` functions that delegate to a `callApi` callback.
|
|
6
|
-
*/
|
|
7
|
-
import { newAsyncContext } from 'quickjs-emscripten';
|
|
8
|
-
const MEMORY_LIMIT = 16 * 1024 * 1024; // 16MB
|
|
9
|
-
const TIMEOUT_MS = 30_000; // 30s
|
|
10
|
-
export async function executeSandboxedCode(code, callApi) {
|
|
11
|
-
const ctx = await newAsyncContext();
|
|
12
|
-
const outputLines = [];
|
|
13
|
-
try {
|
|
14
|
-
// Set memory limit
|
|
15
|
-
const rt = ctx.runtime;
|
|
16
|
-
rt.setMemoryLimit(MEMORY_LIMIT);
|
|
17
|
-
// Set execution timeout
|
|
18
|
-
let expired = false;
|
|
19
|
-
const deadline = Date.now() + TIMEOUT_MS;
|
|
20
|
-
rt.setInterruptHandler(() => {
|
|
21
|
-
if (Date.now() > deadline) {
|
|
22
|
-
expired = true;
|
|
23
|
-
return true; // interrupt
|
|
24
|
-
}
|
|
25
|
-
return false;
|
|
26
|
-
});
|
|
27
|
-
// Inject console.log
|
|
28
|
-
const consoleObj = ctx.newObject();
|
|
29
|
-
const logFn = ctx.newFunction('log', (...args) => {
|
|
30
|
-
const parts = args.map((arg) => {
|
|
31
|
-
const str = ctx.getString(arg);
|
|
32
|
-
return str;
|
|
33
|
-
});
|
|
34
|
-
outputLines.push(parts.join(' '));
|
|
35
|
-
});
|
|
36
|
-
ctx.setProp(consoleObj, 'log', logFn);
|
|
37
|
-
ctx.setProp(ctx.global, 'console', consoleObj);
|
|
38
|
-
logFn.dispose();
|
|
39
|
-
consoleObj.dispose();
|
|
40
|
-
// Inject __callApi as a native async function
|
|
41
|
-
const callApiFn = ctx.newAsyncifiedFunction('__callApi', async (toolHandle, paramsHandle) => {
|
|
42
|
-
const tool = ctx.getString(toolHandle);
|
|
43
|
-
const paramsJson = ctx.getString(paramsHandle);
|
|
44
|
-
const params = paramsJson ? JSON.parse(paramsJson) : {};
|
|
45
|
-
const result = await callApi(tool, params);
|
|
46
|
-
return ctx.newString(JSON.stringify(result));
|
|
47
|
-
});
|
|
48
|
-
ctx.setProp(ctx.global, '__callApi', callApiFn);
|
|
49
|
-
callApiFn.dispose();
|
|
50
|
-
// Wrapper code that creates the warm.* API using __callApi
|
|
51
|
-
const wrappedCode = `
|
|
52
|
-
async function __run() {
|
|
53
|
-
const warm = {
|
|
54
|
-
getAccounts: () => __callApi('get_accounts', '{}').then(JSON.parse),
|
|
55
|
-
getTransactions: (p) => __callApi('get_transactions', JSON.stringify(p || {})).then(JSON.parse),
|
|
56
|
-
getRecurring: () => __callApi('get_recurring', '{}').then(JSON.parse),
|
|
57
|
-
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
|
-
};
|
|
63
|
-
${code}
|
|
64
|
-
}
|
|
65
|
-
__run();
|
|
66
|
-
`;
|
|
67
|
-
const result = await ctx.evalCodeAsync(wrappedCode);
|
|
68
|
-
if (expired) {
|
|
69
|
-
result.dispose();
|
|
70
|
-
return { output: outputLines.join('\n'), error: `Execution timed out after ${TIMEOUT_MS / 1000}s` };
|
|
71
|
-
}
|
|
72
|
-
if (result.error) {
|
|
73
|
-
const errorMsg = ctx.getString(result.error);
|
|
74
|
-
result.error.dispose();
|
|
75
|
-
return { output: outputLines.join('\n'), error: errorMsg };
|
|
76
|
-
}
|
|
77
|
-
// If the result is a promise, await it
|
|
78
|
-
const promiseResult = await ctx.resolvePromise(result.value);
|
|
79
|
-
if (promiseResult.error) {
|
|
80
|
-
const errorMsg = ctx.getString(promiseResult.error);
|
|
81
|
-
promiseResult.error.dispose();
|
|
82
|
-
result.value.dispose();
|
|
83
|
-
return { output: outputLines.join('\n'), error: errorMsg };
|
|
84
|
-
}
|
|
85
|
-
promiseResult.value.dispose();
|
|
86
|
-
result.value.dispose();
|
|
87
|
-
return { output: outputLines.join('\n') };
|
|
88
|
-
}
|
|
89
|
-
finally {
|
|
90
|
-
ctx.dispose();
|
|
91
|
-
}
|
|
92
|
-
}
|