@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 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
- throw new Error(errorMessages[response.status] || `HTTP ${response.status}`);
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
- // EXTRACTED TOOL HANDLERS
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 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;
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 limited = compactTxns.slice(0, requestedLimit);
169
- const truncated = compactTxns.length > requestedLimit;
170
- const result = { summary, txns: limited };
171
- if (truncated) {
172
- result.more = compactTxns.length - requestedLimit;
173
- }
174
- let output = JSON.stringify(result);
175
- if (output.length > MAX_RESPONSE_SIZE) {
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 granularity = args?.granularity || 'daily';
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 { granularity, snapshots: result };
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: '2.0.0' }, { capabilities: { tools: {} } });
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. Use for: "What accounts do I have?", "What is my checking balance?", "Show my credit cards".\nReturns: { accounts: Array<{ name: string; type: string; balance: number; institution: string }> }',
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 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 }',
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 to return (default: 200, max: 1000)',
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 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 }> }',
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: 'Number of snapshots (default: 30)',
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 and working.\nReturns: { valid: boolean; user_id: string }',
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": "2.1.0",
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",
@@ -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
- }