@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 +3 -0
- package/dist/server.js +28 -67
- package/package.json +2 -3
- package/dist/api-types.d.ts +0 -8
- package/dist/api-types.js +0 -33
- package/dist/sandbox.d.ts +0 -10
- package/dist/sandbox.js +0 -87
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
|
|
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;
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
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
|
|
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": "
|
|
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,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
|
-
}
|