@warmio/mcp 1.0.3 → 1.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/server.js +118 -67
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -11,6 +11,39 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as os from 'os';
|
|
13
13
|
const API_URL = process.env.WARM_API_URL || 'https://warm.io';
|
|
14
|
+
const MAX_RESPONSE_SIZE = 50_000;
|
|
15
|
+
function compactTransaction(t) {
|
|
16
|
+
return {
|
|
17
|
+
d: t.date,
|
|
18
|
+
a: t.amount,
|
|
19
|
+
m: t.merchant_name || t.name || 'Unknown',
|
|
20
|
+
c: t.category || null,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function matchesSearch(t, search) {
|
|
24
|
+
const s = search.toLowerCase();
|
|
25
|
+
const merchant = (t.merchant_name || t.name || '').toLowerCase();
|
|
26
|
+
const category = (t.category || '').toLowerCase();
|
|
27
|
+
return merchant.includes(s) || category.includes(s);
|
|
28
|
+
}
|
|
29
|
+
function inDateRange(t, since, until) {
|
|
30
|
+
if (since && t.date < since)
|
|
31
|
+
return false;
|
|
32
|
+
if (until && t.date > until)
|
|
33
|
+
return false;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
function calculateSummary(transactions) {
|
|
37
|
+
if (transactions.length === 0) {
|
|
38
|
+
return { total: 0, count: 0, avg: 0 };
|
|
39
|
+
}
|
|
40
|
+
const total = transactions.reduce((sum, t) => sum + t.a, 0);
|
|
41
|
+
return {
|
|
42
|
+
total: Math.round(total * 100) / 100,
|
|
43
|
+
count: transactions.length,
|
|
44
|
+
avg: Math.round((total / transactions.length) * 100) / 100,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
14
47
|
function getApiKey() {
|
|
15
48
|
if (process.env.WARM_API_KEY) {
|
|
16
49
|
return process.env.WARM_API_KEY;
|
|
@@ -49,85 +82,84 @@ async function apiRequest(endpoint, params = {}) {
|
|
|
49
82
|
}
|
|
50
83
|
return response.json();
|
|
51
84
|
}
|
|
52
|
-
const server = new Server({ name: 'warm', version: '1.0
|
|
85
|
+
const server = new Server({ name: 'warm', version: '1.2.0' }, { capabilities: { tools: {} } });
|
|
53
86
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
54
87
|
tools: [
|
|
55
88
|
{
|
|
56
89
|
name: 'get_accounts',
|
|
57
|
-
description: 'Get all connected bank accounts with balances. Returns
|
|
90
|
+
description: 'Get all connected bank accounts with balances. Use for: "What accounts do I have?", "What is my checking balance?", "Show my credit cards". Returns: array of {name, type, balance, institution}.',
|
|
58
91
|
inputSchema: {
|
|
59
92
|
type: 'object',
|
|
60
|
-
properties: {
|
|
61
|
-
since: {
|
|
62
|
-
type: 'string',
|
|
63
|
-
description: 'Filter accounts updated since this date (ISO format)',
|
|
64
|
-
},
|
|
65
|
-
},
|
|
93
|
+
properties: {},
|
|
66
94
|
},
|
|
95
|
+
annotations: { readOnlyHint: true },
|
|
67
96
|
},
|
|
68
97
|
{
|
|
69
98
|
name: 'get_transactions',
|
|
70
|
-
description: '
|
|
99
|
+
description: 'Search and analyze transactions. Use for: "How much did I spend on X?", "Show my Amazon purchases", "What did I buy last month?". Returns: {summary: {total, count, avg}, txns: [{d, a, m, c}]} where d=date, a=amount, m=merchant, c=category.',
|
|
71
100
|
inputSchema: {
|
|
72
101
|
type: 'object',
|
|
73
102
|
properties: {
|
|
74
|
-
|
|
75
|
-
type: '
|
|
76
|
-
description: '
|
|
103
|
+
search: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Filter by merchant or category (e.g., "coffee", "amazon", "groceries")',
|
|
77
106
|
},
|
|
78
107
|
since: {
|
|
79
108
|
type: 'string',
|
|
80
|
-
description: '
|
|
109
|
+
description: 'Start date inclusive (YYYY-MM-DD)',
|
|
81
110
|
},
|
|
82
|
-
|
|
111
|
+
until: {
|
|
83
112
|
type: 'string',
|
|
84
|
-
description: '
|
|
113
|
+
description: 'End date inclusive (YYYY-MM-DD)',
|
|
114
|
+
},
|
|
115
|
+
limit: {
|
|
116
|
+
type: 'number',
|
|
117
|
+
description: 'Max transactions (default: 50, max: 200)',
|
|
85
118
|
},
|
|
86
119
|
},
|
|
87
120
|
},
|
|
121
|
+
annotations: { readOnlyHint: true },
|
|
88
122
|
},
|
|
89
123
|
{
|
|
90
124
|
name: 'get_recurring',
|
|
91
|
-
description: 'Get recurring payments
|
|
125
|
+
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}]}.',
|
|
92
126
|
inputSchema: {
|
|
93
127
|
type: 'object',
|
|
94
|
-
properties: {
|
|
95
|
-
since: {
|
|
96
|
-
type: 'string',
|
|
97
|
-
description: 'Filter recurring items detected since this date',
|
|
98
|
-
},
|
|
99
|
-
},
|
|
128
|
+
properties: {},
|
|
100
129
|
},
|
|
130
|
+
annotations: { readOnlyHint: true },
|
|
101
131
|
},
|
|
102
132
|
{
|
|
103
133
|
name: 'get_snapshots',
|
|
104
|
-
description: 'Get net worth
|
|
134
|
+
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?". Returns: {snapshots: [{d, nw, a, l}]} where nw=net_worth, a=assets, l=liabilities.',
|
|
105
135
|
inputSchema: {
|
|
106
136
|
type: 'object',
|
|
107
137
|
properties: {
|
|
108
138
|
granularity: {
|
|
109
139
|
type: 'string',
|
|
110
140
|
enum: ['daily', 'monthly'],
|
|
111
|
-
description: '
|
|
141
|
+
description: 'daily or monthly (default: daily)',
|
|
112
142
|
},
|
|
113
143
|
limit: {
|
|
114
144
|
type: 'number',
|
|
115
|
-
description: 'Number of snapshots
|
|
145
|
+
description: 'Number of snapshots (default: 30)',
|
|
116
146
|
},
|
|
117
147
|
since: {
|
|
118
148
|
type: 'string',
|
|
119
|
-
description: 'Start date
|
|
149
|
+
description: 'Start date (YYYY-MM-DD)',
|
|
120
150
|
},
|
|
121
151
|
},
|
|
122
152
|
},
|
|
153
|
+
annotations: { readOnlyHint: true },
|
|
123
154
|
},
|
|
124
155
|
{
|
|
125
156
|
name: 'verify_key',
|
|
126
|
-
description: 'Check if
|
|
157
|
+
description: 'Check if API key is valid and working.',
|
|
127
158
|
inputSchema: {
|
|
128
159
|
type: 'object',
|
|
129
160
|
properties: {},
|
|
130
161
|
},
|
|
162
|
+
annotations: { readOnlyHint: true },
|
|
131
163
|
},
|
|
132
164
|
],
|
|
133
165
|
}));
|
|
@@ -136,37 +168,62 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
136
168
|
try {
|
|
137
169
|
switch (name) {
|
|
138
170
|
case 'get_accounts': {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
params.since = String(args.since);
|
|
142
|
-
const data = await apiRequest('/api/accounts', params);
|
|
143
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
171
|
+
const data = await apiRequest('/api/accounts');
|
|
172
|
+
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
144
173
|
}
|
|
145
174
|
case 'get_transactions': {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
175
|
+
const search = args?.search ? String(args.search) : undefined;
|
|
176
|
+
const since = args?.since ? String(args.since) : undefined;
|
|
177
|
+
const until = args?.until ? String(args.until) : undefined;
|
|
178
|
+
const requestedLimit = args?.limit ? Math.min(Number(args.limit), 200) : 50;
|
|
179
|
+
// Fetch more if filtering, since we'll reduce the set
|
|
180
|
+
const fetchLimit = search || until ? Math.min(requestedLimit * 10, 1000) : requestedLimit;
|
|
181
|
+
const params = { limit: String(fetchLimit) };
|
|
182
|
+
if (since)
|
|
183
|
+
params.last_knowledge = since;
|
|
184
|
+
const response = (await apiRequest('/api/transactions', params));
|
|
185
|
+
let transactions = (response.transactions || []);
|
|
186
|
+
// Apply date range filter (until is client-side since API only supports since)
|
|
187
|
+
if (until) {
|
|
188
|
+
transactions = transactions.filter((t) => inDateRange(t, since, until));
|
|
189
|
+
}
|
|
190
|
+
// Apply search filter
|
|
191
|
+
if (search) {
|
|
192
|
+
transactions = transactions.filter((t) => matchesSearch(t, search));
|
|
193
|
+
}
|
|
194
|
+
// Convert to compact format
|
|
195
|
+
const compactTxns = transactions.map(compactTransaction);
|
|
196
|
+
// Calculate summary on ALL matching transactions
|
|
197
|
+
const summary = calculateSummary(compactTxns);
|
|
198
|
+
// Apply limit for display
|
|
199
|
+
const limited = compactTxns.slice(0, requestedLimit);
|
|
200
|
+
const truncated = compactTxns.length > requestedLimit;
|
|
201
|
+
// Build compact result
|
|
202
|
+
const result = { summary, txns: limited };
|
|
203
|
+
if (truncated) {
|
|
204
|
+
result.more = compactTxns.length - requestedLimit;
|
|
205
|
+
}
|
|
206
|
+
// Size check and reduce if needed
|
|
207
|
+
let output = JSON.stringify(result);
|
|
208
|
+
if (output.length > MAX_RESPONSE_SIZE) {
|
|
209
|
+
const reducedCount = Math.floor(limited.length * (MAX_RESPONSE_SIZE / output.length) * 0.8);
|
|
210
|
+
result.txns = limited.slice(0, reducedCount);
|
|
211
|
+
result.more = compactTxns.length - reducedCount;
|
|
212
|
+
output = JSON.stringify(result);
|
|
213
|
+
}
|
|
214
|
+
return { content: [{ type: 'text', text: output }] };
|
|
155
215
|
}
|
|
156
216
|
case 'get_recurring': {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const data = await apiRequest('/api/recurring', params);
|
|
161
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
217
|
+
const response = (await apiRequest('/api/transactions', { limit: '1' }));
|
|
218
|
+
const recurring = response.recurring || [];
|
|
219
|
+
return { content: [{ type: 'text', text: JSON.stringify({ recurring }) }] };
|
|
162
220
|
}
|
|
163
221
|
case 'get_snapshots': {
|
|
164
|
-
const response = (await apiRequest('/api/transactions', {
|
|
165
|
-
limit: '1',
|
|
166
|
-
}));
|
|
222
|
+
const response = (await apiRequest('/api/transactions', { limit: '1' }));
|
|
167
223
|
const snapshots = response.snapshots || [];
|
|
168
224
|
const granularity = args?.granularity || 'daily';
|
|
169
|
-
const
|
|
225
|
+
const defaultLimit = granularity === 'daily' ? 30 : 0;
|
|
226
|
+
const limit = args?.limit ? Number(args.limit) : defaultLimit;
|
|
170
227
|
const since = args?.since;
|
|
171
228
|
let filtered = snapshots;
|
|
172
229
|
if (since) {
|
|
@@ -176,8 +233,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
176
233
|
const byMonth = new Map();
|
|
177
234
|
filtered.forEach((s) => {
|
|
178
235
|
const month = String(s.snapshot_date).substring(0, 7);
|
|
179
|
-
if (!byMonth.has(month) ||
|
|
180
|
-
String(s.snapshot_date) > String(byMonth.get(month).snapshot_date)) {
|
|
236
|
+
if (!byMonth.has(month) || String(s.snapshot_date) > String(byMonth.get(month).snapshot_date)) {
|
|
181
237
|
byMonth.set(month, s);
|
|
182
238
|
}
|
|
183
239
|
});
|
|
@@ -187,23 +243,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
187
243
|
if (limit > 0) {
|
|
188
244
|
filtered = filtered.slice(0, limit);
|
|
189
245
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
total_assets: s.total_assets,
|
|
199
|
-
total_liabilities: s.total_liabilities,
|
|
200
|
-
})),
|
|
201
|
-
};
|
|
202
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
246
|
+
// Compact output
|
|
247
|
+
const result = filtered.map((s) => ({
|
|
248
|
+
d: s.snapshot_date,
|
|
249
|
+
nw: s.net_worth,
|
|
250
|
+
a: s.total_assets,
|
|
251
|
+
l: s.total_liabilities,
|
|
252
|
+
}));
|
|
253
|
+
return { content: [{ type: 'text', text: JSON.stringify({ granularity, snapshots: result }) }] };
|
|
203
254
|
}
|
|
204
255
|
case 'verify_key': {
|
|
205
256
|
const data = await apiRequest('/api/verify');
|
|
206
|
-
return { content: [{ type: 'text', text: JSON.stringify(data
|
|
257
|
+
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
207
258
|
}
|
|
208
259
|
default:
|
|
209
260
|
throw new Error(`Unknown tool: ${name}`);
|
|
@@ -212,7 +263,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
212
263
|
catch (error) {
|
|
213
264
|
const message = error instanceof Error ? error.message : String(error);
|
|
214
265
|
return {
|
|
215
|
-
content: [{ type: 'text', text: JSON.stringify({ error: message }
|
|
266
|
+
content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
|
|
216
267
|
isError: true,
|
|
217
268
|
};
|
|
218
269
|
}
|