@warmio/mcp 1.2.3 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-types.d.ts +8 -0
- package/dist/api-types.js +33 -0
- package/dist/sandbox.d.ts +10 -0
- package/dist/sandbox.js +87 -0
- package/dist/server.js +166 -129
- package/package.json +3 -2
|
@@ -0,0 +1,8 @@
|
|
|
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;
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -10,6 +10,8 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as os from 'os';
|
|
13
|
+
import { generateApiTypeString } from './api-types.js';
|
|
14
|
+
import { executeSandboxedCode } from './sandbox.js';
|
|
13
15
|
const API_URL = process.env.WARM_API_URL || 'https://warm.io';
|
|
14
16
|
const MAX_RESPONSE_SIZE = 50_000;
|
|
15
17
|
const MAX_TRANSACTION_PAGES = 10;
|
|
@@ -22,13 +24,17 @@ const REQUEST_TIMEOUT_MS = (() => {
|
|
|
22
24
|
let cachedApiKey;
|
|
23
25
|
function compactTransaction(t) {
|
|
24
26
|
return {
|
|
25
|
-
d: t.date,
|
|
26
|
-
|
|
27
|
+
d: t.date || '',
|
|
28
|
+
// Positive = expense, negative = income/deposit (Plaid convention)
|
|
29
|
+
a: t.amount ? Math.round(t.amount * 100) / 100 : 0,
|
|
27
30
|
m: t.merchant_name || t.name || 'Unknown',
|
|
28
|
-
|
|
31
|
+
// Include category (null if not set)
|
|
32
|
+
c: t.primary_category ?? null,
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
function inDateRange(t, since, until) {
|
|
36
|
+
if (!t.date)
|
|
37
|
+
return false;
|
|
32
38
|
if (since && t.date < since)
|
|
33
39
|
return false;
|
|
34
40
|
if (until && t.date > until)
|
|
@@ -107,16 +113,123 @@ async function apiRequest(endpoint, params = {}) {
|
|
|
107
113
|
403: 'Pro subscription required. Upgrade at https://warm.io/settings',
|
|
108
114
|
429: 'Rate limit exceeded. Try again in a few minutes.',
|
|
109
115
|
};
|
|
110
|
-
|
|
116
|
+
if (errorMessages[response.status]) {
|
|
117
|
+
throw new Error(errorMessages[response.status]);
|
|
118
|
+
}
|
|
119
|
+
// Read the actual error message from the API response body
|
|
120
|
+
let detail = `HTTP ${response.status}`;
|
|
121
|
+
try {
|
|
122
|
+
const body = (await response.json());
|
|
123
|
+
if (body?.error)
|
|
124
|
+
detail = body.error;
|
|
125
|
+
}
|
|
126
|
+
catch { /* ignore parse failures */ }
|
|
127
|
+
throw new Error(detail);
|
|
111
128
|
}
|
|
112
129
|
return response.json();
|
|
113
130
|
}
|
|
114
|
-
|
|
131
|
+
// ============================================
|
|
132
|
+
// TOOL HANDLERS
|
|
133
|
+
// ============================================
|
|
134
|
+
async function handleGetAccounts() {
|
|
135
|
+
const response = (await apiRequest('/api/accounts'));
|
|
136
|
+
return {
|
|
137
|
+
accounts: response.accounts || [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function handleGetTransactions(args) {
|
|
141
|
+
const since = args?.since ? String(args.since) : undefined;
|
|
142
|
+
const until = args?.until ? String(args.until) : undefined;
|
|
143
|
+
let transactions = [];
|
|
144
|
+
let cursor;
|
|
145
|
+
let pagesFetched = 0;
|
|
146
|
+
let scanned = 0;
|
|
147
|
+
do {
|
|
148
|
+
const params = {
|
|
149
|
+
limit: String(TRANSACTION_PAGE_SIZE),
|
|
150
|
+
};
|
|
151
|
+
// API rejects last_knowledge + cursor together; only use last_knowledge on first page
|
|
152
|
+
if (since && !cursor)
|
|
153
|
+
params.last_knowledge = since;
|
|
154
|
+
if (cursor)
|
|
155
|
+
params.cursor = cursor;
|
|
156
|
+
const response = (await apiRequest('/api/transactions', params));
|
|
157
|
+
const batch = (response.transactions || []);
|
|
158
|
+
transactions.push(...batch);
|
|
159
|
+
scanned += batch.length;
|
|
160
|
+
pagesFetched += 1;
|
|
161
|
+
cursor = response.pagination?.next_cursor ?? undefined;
|
|
162
|
+
} while (cursor && pagesFetched < MAX_TRANSACTION_PAGES && scanned < MAX_TRANSACTION_SCAN);
|
|
163
|
+
if (until) {
|
|
164
|
+
transactions = transactions.filter((t) => inDateRange(t, since, until));
|
|
165
|
+
}
|
|
166
|
+
const compactTxns = transactions.map(compactTransaction);
|
|
167
|
+
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;
|
|
181
|
+
}
|
|
182
|
+
async function handleGetSnapshots(args) {
|
|
183
|
+
const response = (await apiRequest('/api/snapshots'));
|
|
184
|
+
const snapshots = response.snapshots || [];
|
|
185
|
+
const limit = args?.limit ? Number(args.limit) : 30;
|
|
186
|
+
const since = args?.since;
|
|
187
|
+
// Normalize snapshot dates (support both snapshot_date and d)
|
|
188
|
+
const normalized = snapshots.map((s) => ({
|
|
189
|
+
date: s.snapshot_date || s.d || '',
|
|
190
|
+
net_worth: s.net_worth ?? s.nw ?? 0,
|
|
191
|
+
total_assets: s.total_assets ?? s.a ?? 0,
|
|
192
|
+
total_liabilities: s.total_liabilities ?? s.l ?? 0,
|
|
193
|
+
}));
|
|
194
|
+
let filtered = normalized;
|
|
195
|
+
if (since) {
|
|
196
|
+
filtered = filtered.filter((s) => s.date >= since);
|
|
197
|
+
}
|
|
198
|
+
filtered.sort((a, b) => b.date.localeCompare(a.date));
|
|
199
|
+
if (limit > 0) {
|
|
200
|
+
filtered = filtered.slice(0, limit);
|
|
201
|
+
}
|
|
202
|
+
const result = filtered.map((s) => ({
|
|
203
|
+
d: s.date,
|
|
204
|
+
nw: Math.round(s.net_worth * 100) / 100,
|
|
205
|
+
a: Math.round(s.total_assets * 100) / 100,
|
|
206
|
+
l: Math.round(s.total_liabilities * 100) / 100,
|
|
207
|
+
}));
|
|
208
|
+
return { snapshots: result };
|
|
209
|
+
}
|
|
210
|
+
async function handleVerifyKey() {
|
|
211
|
+
const response = (await apiRequest('/api/verify'));
|
|
212
|
+
return {
|
|
213
|
+
valid: response.valid === true,
|
|
214
|
+
status: response.status || (response.valid ? 'ok' : 'invalid'),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// Tool name → handler mapping for sandbox dispatch
|
|
218
|
+
const toolHandlers = {
|
|
219
|
+
get_accounts: handleGetAccounts,
|
|
220
|
+
get_transactions: handleGetTransactions,
|
|
221
|
+
get_snapshots: handleGetSnapshots,
|
|
222
|
+
verify_key: handleVerifyKey,
|
|
223
|
+
};
|
|
224
|
+
// ============================================
|
|
225
|
+
// SERVER SETUP
|
|
226
|
+
// ============================================
|
|
227
|
+
const server = new Server({ name: 'warm', version: '2.0.0' }, { capabilities: { tools: {} } });
|
|
115
228
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
116
229
|
tools: [
|
|
117
230
|
{
|
|
118
231
|
name: 'get_accounts',
|
|
119
|
-
description: 'Get all connected bank accounts with balances. Use for: "What accounts do I have?", "What is my checking balance?", "Show my credit cards"
|
|
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 }> }',
|
|
120
233
|
inputSchema: {
|
|
121
234
|
type: 'object',
|
|
122
235
|
properties: {},
|
|
@@ -125,49 +238,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
125
238
|
},
|
|
126
239
|
{
|
|
127
240
|
name: 'get_transactions',
|
|
128
|
-
description: '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 }',
|
|
129
242
|
inputSchema: {
|
|
130
243
|
type: 'object',
|
|
131
244
|
properties: {
|
|
132
245
|
since: {
|
|
133
246
|
type: 'string',
|
|
134
|
-
description: 'Start date inclusive (YYYY-MM-DD)',
|
|
247
|
+
description: 'Start date inclusive (YYYY-MM-DD). Omit to get all available transactions.',
|
|
135
248
|
},
|
|
136
249
|
until: {
|
|
137
250
|
type: 'string',
|
|
138
|
-
description: 'End date inclusive (YYYY-MM-DD)',
|
|
139
|
-
},
|
|
140
|
-
limit: {
|
|
141
|
-
type: 'number',
|
|
142
|
-
description: 'Max transactions to return (default: 200, max: 1000)',
|
|
251
|
+
description: 'End date inclusive (YYYY-MM-DD). Omit for no end date filter.',
|
|
143
252
|
},
|
|
144
253
|
},
|
|
145
254
|
},
|
|
146
255
|
annotations: { readOnlyHint: true },
|
|
147
256
|
},
|
|
148
|
-
{
|
|
149
|
-
name: 'get_recurring',
|
|
150
|
-
description: 'Get detected subscriptions and recurring payments. Use for: "What subscriptions do I have?", "Show my monthly bills", "What are my recurring charges?". Returns: {recurring: [{merchant, amount, frequency, next_date}]}.',
|
|
151
|
-
inputSchema: {
|
|
152
|
-
type: 'object',
|
|
153
|
-
properties: {},
|
|
154
|
-
},
|
|
155
|
-
annotations: { readOnlyHint: true },
|
|
156
|
-
},
|
|
157
257
|
{
|
|
158
258
|
name: 'get_snapshots',
|
|
159
|
-
description: 'Get net worth history
|
|
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 }> }',
|
|
160
260
|
inputSchema: {
|
|
161
261
|
type: 'object',
|
|
162
262
|
properties: {
|
|
163
|
-
granularity: {
|
|
164
|
-
type: 'string',
|
|
165
|
-
enum: ['daily', 'monthly'],
|
|
166
|
-
description: 'daily or monthly (default: daily)',
|
|
167
|
-
},
|
|
168
263
|
limit: {
|
|
169
264
|
type: 'number',
|
|
170
|
-
description: 'Number of snapshots (default: 30)',
|
|
265
|
+
description: 'Number of daily snapshots to return (default: 30)',
|
|
171
266
|
},
|
|
172
267
|
since: {
|
|
173
268
|
type: 'string',
|
|
@@ -177,9 +272,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
177
272
|
},
|
|
178
273
|
annotations: { readOnlyHint: true },
|
|
179
274
|
},
|
|
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
|
+
},
|
|
180
290
|
{
|
|
181
291
|
name: 'verify_key',
|
|
182
|
-
description: 'Check if API key is valid and working
|
|
292
|
+
description: 'Check if API key is valid and working.\nReturns: { valid: boolean; status: string }',
|
|
183
293
|
inputSchema: {
|
|
184
294
|
type: 'object',
|
|
185
295
|
properties: {},
|
|
@@ -191,108 +301,35 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
191
301
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
192
302
|
const { name, arguments: args } = request.params;
|
|
193
303
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
case 'get_transactions': {
|
|
200
|
-
const since = args?.since ? String(args.since) : undefined;
|
|
201
|
-
const until = args?.until ? String(args.until) : undefined;
|
|
202
|
-
const parsedLimit = args?.limit ? Number(args.limit) : 200;
|
|
203
|
-
const requestedLimit = Number.isFinite(parsedLimit)
|
|
204
|
-
? Math.max(1, Math.min(Math.floor(parsedLimit), 1000))
|
|
205
|
-
: 200;
|
|
206
|
-
let transactions = [];
|
|
207
|
-
let cursor;
|
|
208
|
-
let pagesFetched = 0;
|
|
209
|
-
let scanned = 0;
|
|
210
|
-
do {
|
|
211
|
-
const params = {
|
|
212
|
-
limit: String(TRANSACTION_PAGE_SIZE),
|
|
213
|
-
};
|
|
214
|
-
if (since)
|
|
215
|
-
params.last_knowledge = since;
|
|
216
|
-
if (cursor)
|
|
217
|
-
params.cursor = cursor;
|
|
218
|
-
const response = (await apiRequest('/api/transactions', params));
|
|
219
|
-
const batch = (response.transactions || []);
|
|
220
|
-
transactions.push(...batch);
|
|
221
|
-
scanned += batch.length;
|
|
222
|
-
pagesFetched += 1;
|
|
223
|
-
cursor = response.cursor;
|
|
224
|
-
} while (cursor && pagesFetched < MAX_TRANSACTION_PAGES && scanned < MAX_TRANSACTION_SCAN);
|
|
225
|
-
// Apply date range filter (until is client-side since API only supports since)
|
|
226
|
-
if (until) {
|
|
227
|
-
transactions = transactions.filter((t) => inDateRange(t, since, until));
|
|
228
|
-
}
|
|
229
|
-
const compactTxns = transactions.map(compactTransaction);
|
|
230
|
-
// Calculate summary on ALL matching transactions
|
|
231
|
-
const summary = calculateSummary(compactTxns);
|
|
232
|
-
// Apply limit for display
|
|
233
|
-
const limited = compactTxns.slice(0, requestedLimit);
|
|
234
|
-
const truncated = compactTxns.length > requestedLimit;
|
|
235
|
-
// Build compact result
|
|
236
|
-
const result = { summary, txns: limited };
|
|
237
|
-
if (truncated) {
|
|
238
|
-
result.more = compactTxns.length - requestedLimit;
|
|
239
|
-
}
|
|
240
|
-
// Size check and reduce if needed
|
|
241
|
-
let output = JSON.stringify(result);
|
|
242
|
-
if (output.length > MAX_RESPONSE_SIZE) {
|
|
243
|
-
const reducedCount = Math.floor(limited.length * (MAX_RESPONSE_SIZE / output.length) * 0.8);
|
|
244
|
-
result.txns = limited.slice(0, reducedCount);
|
|
245
|
-
result.more = compactTxns.length - reducedCount;
|
|
246
|
-
output = JSON.stringify(result);
|
|
247
|
-
}
|
|
248
|
-
return { content: [{ type: 'text', text: output }] };
|
|
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');
|
|
249
309
|
}
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
case 'get_snapshots': {
|
|
256
|
-
const response = (await apiRequest('/api/transactions', { limit: '1' }));
|
|
257
|
-
const snapshots = response.snapshots || [];
|
|
258
|
-
const granularity = args?.granularity || 'daily';
|
|
259
|
-
const defaultLimit = granularity === 'daily' ? 30 : 0;
|
|
260
|
-
const limit = args?.limit ? Number(args.limit) : defaultLimit;
|
|
261
|
-
const since = args?.since;
|
|
262
|
-
let filtered = snapshots;
|
|
263
|
-
if (since) {
|
|
264
|
-
filtered = filtered.filter((s) => String(s.snapshot_date) >= since);
|
|
265
|
-
}
|
|
266
|
-
if (granularity === 'monthly') {
|
|
267
|
-
const byMonth = new Map();
|
|
268
|
-
filtered.forEach((s) => {
|
|
269
|
-
const month = String(s.snapshot_date).substring(0, 7);
|
|
270
|
-
if (!byMonth.has(month) || String(s.snapshot_date) > String(byMonth.get(month).snapshot_date)) {
|
|
271
|
-
byMonth.set(month, s);
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
filtered = Array.from(byMonth.values());
|
|
275
|
-
}
|
|
276
|
-
filtered.sort((a, b) => String(b.snapshot_date).localeCompare(String(a.snapshot_date)));
|
|
277
|
-
if (limit > 0) {
|
|
278
|
-
filtered = filtered.slice(0, limit);
|
|
310
|
+
const callApi = async (tool, params) => {
|
|
311
|
+
const handler = toolHandlers[tool];
|
|
312
|
+
if (!handler) {
|
|
313
|
+
throw new Error(`Unknown tool: ${tool}`);
|
|
279
314
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
const handler = toolHandlers[name];
|
|
328
|
+
if (!handler) {
|
|
329
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
295
330
|
}
|
|
331
|
+
const data = await handler(args);
|
|
332
|
+
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
296
333
|
}
|
|
297
334
|
catch (error) {
|
|
298
335
|
const message = error instanceof Error ? error.message : String(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warmio/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "MCP server for Warm Financial API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
],
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/sdk": "^1.25.0"
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.25.0",
|
|
27
|
+
"quickjs-emscripten": "^0.31.0"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@types/node": "^22.0.0",
|