@warmio/mcp 2.2.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
  }
@@ -116,7 +114,6 @@ async function apiRequest(endpoint, params = {}) {
116
114
  if (errorMessages[response.status]) {
117
115
  throw new Error(errorMessages[response.status]);
118
116
  }
119
- // Read the actual error message from the API response body
120
117
  let detail = `HTTP ${response.status}`;
121
118
  try {
122
119
  const body = (await response.json());
@@ -140,6 +137,8 @@ async function handleGetAccounts() {
140
137
  async function handleGetTransactions(args) {
141
138
  const since = args?.since ? String(args.since) : undefined;
142
139
  const until = args?.until ? String(args.until) : undefined;
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);
143
142
  let transactions = [];
144
143
  let cursor;
145
144
  let pagesFetched = 0;
@@ -148,7 +147,6 @@ async function handleGetTransactions(args) {
148
147
  const params = {
149
148
  limit: String(TRANSACTION_PAGE_SIZE),
150
149
  };
151
- // API rejects last_knowledge + cursor together; only use last_knowledge on first page
152
150
  if (since && !cursor)
153
151
  params.last_knowledge = since;
154
152
  if (cursor)
@@ -165,26 +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, 1000);
169
- const truncated = compactTxns.length > 1000;
170
- const result = { summary, txns: limited };
171
- if (truncated) {
172
- result.more = compactTxns.length - 1000;
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;
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
+ };
181
174
  }
182
175
  async function handleGetSnapshots(args) {
183
176
  const response = (await apiRequest('/api/snapshots'));
184
177
  const snapshots = response.snapshots || [];
185
178
  const limit = args?.limit ? Number(args.limit) : 30;
186
179
  const since = args?.since;
187
- // Normalize snapshot dates (support both snapshot_date and d)
188
180
  const normalized = snapshots.map((s) => ({
189
181
  date: s.snapshot_date || s.d || '',
190
182
  net_worth: s.net_worth ?? s.nw ?? 0,
@@ -214,7 +206,6 @@ async function handleVerifyKey() {
214
206
  status: response.status || (response.valid ? 'ok' : 'invalid'),
215
207
  };
216
208
  }
217
- // Tool name → handler mapping for sandbox dispatch
218
209
  const toolHandlers = {
219
210
  get_accounts: handleGetAccounts,
220
211
  get_transactions: handleGetTransactions,
@@ -224,12 +215,12 @@ const toolHandlers = {
224
215
  // ============================================
225
216
  // SERVER SETUP
226
217
  // ============================================
227
- 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: {} } });
228
219
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
229
220
  tools: [
230
221
  {
231
222
  name: 'get_accounts',
232
- 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.',
233
224
  inputSchema: {
234
225
  type: 'object',
235
226
  properties: {},
@@ -238,7 +229,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
238
229
  },
239
230
  {
240
231
  name: 'get_transactions',
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 }',
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.',
242
233
  inputSchema: {
243
234
  type: 'object',
244
235
  properties: {
@@ -250,46 +241,39 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
250
241
  type: 'string',
251
242
  description: 'End date inclusive (YYYY-MM-DD). Omit for no end date filter.',
252
243
  },
244
+ limit: {
245
+ type: 'number',
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).',
251
+ },
253
252
  },
254
253
  },
255
254
  annotations: { readOnlyHint: true },
256
255
  },
257
256
  {
258
257
  name: 'get_snapshots',
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 }> }',
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.',
260
259
  inputSchema: {
261
260
  type: 'object',
262
261
  properties: {
263
262
  limit: {
264
263
  type: 'number',
265
- description: 'Number of daily snapshots to return (default: 30)',
264
+ description: 'Max snapshots to return (default 30).',
266
265
  },
267
266
  since: {
268
267
  type: 'string',
269
- description: 'Start date (YYYY-MM-DD)',
268
+ description: 'Start date inclusive (YYYY-MM-DD).',
270
269
  },
271
270
  },
272
271
  },
273
272
  annotations: { readOnlyHint: true },
274
273
  },
275
- {
276
- name: 'run_analysis',
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 }));`,
278
- inputSchema: {
279
- type: 'object',
280
- properties: {
281
- code: {
282
- type: 'string',
283
- description: 'JavaScript code to execute. Use warm.* functions and console.log() for output.',
284
- },
285
- },
286
- required: ['code'],
287
- },
288
- annotations: { readOnlyHint: true },
289
- },
290
274
  {
291
275
  name: 'verify_key',
292
- description: 'Check if API key is valid and working.\nReturns: { valid: boolean; status: string }',
276
+ description: 'Check if the API key is valid.\n\nReturns: { valid: boolean; status: string }',
293
277
  inputSchema: {
294
278
  type: 'object',
295
279
  properties: {},
@@ -301,29 +285,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
301
285
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
302
286
  const { name, arguments: args } = request.params;
303
287
  try {
304
- // Handle run_analysis separately (sandbox execution)
305
- if (name === 'run_analysis') {
306
- const code = args?.code ? String(args.code) : '';
307
- if (!code) {
308
- throw new Error('Code parameter is required');
309
- }
310
- const callApi = async (tool, params) => {
311
- const handler = toolHandlers[tool];
312
- if (!handler) {
313
- throw new Error(`Unknown tool: ${tool}`);
314
- }
315
- return handler(params);
316
- };
317
- const result = await executeSandboxedCode(code, callApi);
318
- const text = result.error
319
- ? `Output:\n${result.output}\n\nError: ${result.error}`
320
- : result.output;
321
- return {
322
- content: [{ type: 'text', text }],
323
- isError: !!result.error,
324
- };
325
- }
326
- // Standard tool dispatch
327
288
  const handler = toolHandlers[name];
328
289
  if (!handler) {
329
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.2.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,33 +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 (up to 1000). 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
- }): Promise<{
20
- summary: { total: number; count: number; avg: number };
21
- txns: Array<{ d: string; a: number; m: string; c: string | null }>;
22
- more?: number;
23
- }>;
24
-
25
- /** Get daily net worth history */
26
- getSnapshots(params?: {
27
- limit?: number;
28
- since?: string;
29
- }): Promise<{
30
- snapshots: Array<{ d: string; nw: number; a: number; l: number }>;
31
- }>;
32
- };`;
33
- }
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,87 +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
- getSnapshots: (p) => __callApi('get_snapshots', JSON.stringify(p || {})).then(JSON.parse),
57
- };
58
- ${code}
59
- }
60
- __run();
61
- `;
62
- const result = await ctx.evalCodeAsync(wrappedCode);
63
- if (expired) {
64
- result.dispose();
65
- return { output: outputLines.join('\n'), error: `Execution timed out after ${TIMEOUT_MS / 1000}s` };
66
- }
67
- if (result.error) {
68
- const errorMsg = ctx.getString(result.error);
69
- result.error.dispose();
70
- return { output: outputLines.join('\n'), error: errorMsg };
71
- }
72
- // If the result is a promise, await it
73
- const promiseResult = await ctx.resolvePromise(result.value);
74
- if (promiseResult.error) {
75
- const errorMsg = ctx.getString(promiseResult.error);
76
- promiseResult.error.dispose();
77
- result.value.dispose();
78
- return { output: outputLines.join('\n'), error: errorMsg };
79
- }
80
- promiseResult.value.dispose();
81
- result.value.dispose();
82
- return { output: outputLines.join('\n') };
83
- }
84
- finally {
85
- ctx.dispose();
86
- }
87
- }