@warmio/mcp 1.0.2 → 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/install.js +41 -14
- package/dist/server.js +118 -67
- package/package.json +1 -1
package/dist/install.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
-
import { join, dirname } from 'path';
|
|
2
|
+
import { join, dirname, resolve } from 'path';
|
|
3
3
|
import { homedir, platform } from 'os';
|
|
4
4
|
import { createInterface } from 'readline';
|
|
5
5
|
const HOME = homedir();
|
|
6
|
+
const CWD = process.cwd();
|
|
6
7
|
function getClaudeDesktopPath() {
|
|
7
8
|
if (platform() === 'win32') {
|
|
8
9
|
return join(process.env.APPDATA || join(HOME, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
|
|
@@ -12,7 +13,7 @@ function getClaudeDesktopPath() {
|
|
|
12
13
|
}
|
|
13
14
|
return join(HOME, '.config', 'claude', 'claude_desktop_config.json');
|
|
14
15
|
}
|
|
15
|
-
const
|
|
16
|
+
const GLOBAL_CLIENTS = [
|
|
16
17
|
{ name: 'Claude Code', configPath: join(HOME, '.claude.json'), format: 'json', alwaysInclude: true },
|
|
17
18
|
{ name: 'Claude Desktop', configPath: getClaudeDesktopPath(), format: 'json' },
|
|
18
19
|
{ name: 'Cursor', configPath: join(HOME, '.cursor', 'mcp.json'), format: 'json' },
|
|
@@ -22,11 +23,28 @@ const ALL_CLIENTS = [
|
|
|
22
23
|
{ name: 'Antigravity', configPath: join(HOME, '.gemini', 'antigravity', 'mcp_config.json'), format: 'json' },
|
|
23
24
|
{ name: 'Gemini CLI', configPath: join(HOME, '.gemini', 'settings.json'), format: 'json' },
|
|
24
25
|
];
|
|
26
|
+
// Project-level MCP config files (checked in CWD)
|
|
27
|
+
const PROJECT_CONFIGS = ['.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json'];
|
|
25
28
|
// On Windows, npx doesn't forward stdin/stdout properly for MCP's JSON-RPC protocol.
|
|
26
29
|
// Using cmd /c npx ... fixes the pipe forwarding.
|
|
27
30
|
const MCP_CONFIG = platform() === 'win32'
|
|
28
31
|
? { command: 'cmd', args: ['/c', 'npx', '-y', '@warmio/mcp', '--server'] }
|
|
29
32
|
: { command: 'npx', args: ['-y', '@warmio/mcp', '--server'] };
|
|
33
|
+
function detectProjectClients() {
|
|
34
|
+
const found = [];
|
|
35
|
+
for (const name of PROJECT_CONFIGS) {
|
|
36
|
+
const configPath = resolve(CWD, name);
|
|
37
|
+
if (existsSync(configPath)) {
|
|
38
|
+
found.push({
|
|
39
|
+
name: `Project (${name})`,
|
|
40
|
+
configPath,
|
|
41
|
+
format: 'json',
|
|
42
|
+
isProjectLevel: true,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
30
48
|
function isDetected(client) {
|
|
31
49
|
if (client.alwaysInclude)
|
|
32
50
|
return true;
|
|
@@ -55,10 +73,16 @@ function configureJson(client, apiKey) {
|
|
|
55
73
|
}
|
|
56
74
|
if (!config.mcpServers)
|
|
57
75
|
config.mcpServers = {};
|
|
58
|
-
config.mcpServers
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
const servers = config.mcpServers;
|
|
77
|
+
const existing = servers.warm;
|
|
78
|
+
// For project-level configs, preserve existing command/args if present — only inject the key.
|
|
79
|
+
if (client.isProjectLevel && existing?.command) {
|
|
80
|
+
existing.env = { WARM_API_KEY: apiKey };
|
|
81
|
+
servers.warm = existing;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
servers.warm = { ...MCP_CONFIG, env: { WARM_API_KEY: apiKey } };
|
|
85
|
+
}
|
|
62
86
|
mkdirSync(dirname(client.configPath), { recursive: true });
|
|
63
87
|
writeFileSync(client.configPath, JSON.stringify(config, null, 2) + '\n');
|
|
64
88
|
}
|
|
@@ -84,7 +108,7 @@ function configure(client, apiKey) {
|
|
|
84
108
|
configureToml(client, apiKey);
|
|
85
109
|
}
|
|
86
110
|
function shortPath(p) {
|
|
87
|
-
return p.replace(HOME, '~');
|
|
111
|
+
return p.replace(HOME, '~').replace(CWD, '.');
|
|
88
112
|
}
|
|
89
113
|
function prompt(question) {
|
|
90
114
|
return new Promise((resolve) => {
|
|
@@ -101,14 +125,17 @@ export async function install() {
|
|
|
101
125
|
console.log(' Warm MCP Server Installer');
|
|
102
126
|
console.log(' -------------------------');
|
|
103
127
|
console.log('');
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
128
|
+
// Detect global + project-level clients
|
|
129
|
+
const globalClients = GLOBAL_CLIENTS.filter(isDetected);
|
|
130
|
+
const projectClients = detectProjectClients();
|
|
131
|
+
const allClients = [...globalClients, ...projectClients];
|
|
132
|
+
const needsSetup = allClients.filter((c) => !isConfigured(c) || force);
|
|
133
|
+
// Show all detected clients
|
|
107
134
|
console.log(' MCP clients found:');
|
|
108
|
-
|
|
135
|
+
allClients.forEach((client) => {
|
|
109
136
|
const configured = isConfigured(client);
|
|
110
137
|
const status = configured && !force ? 'configured' : 'not configured';
|
|
111
|
-
console.log(` ${client.name.padEnd(
|
|
138
|
+
console.log(` ${client.name.padEnd(22)} ${shortPath(client.configPath).padEnd(55)} ${status}`);
|
|
112
139
|
});
|
|
113
140
|
console.log('');
|
|
114
141
|
// Nothing to do
|
|
@@ -132,10 +159,10 @@ export async function install() {
|
|
|
132
159
|
needsSetup.forEach((client) => {
|
|
133
160
|
try {
|
|
134
161
|
configure(client, apiKey);
|
|
135
|
-
console.log(` ${client.name.padEnd(
|
|
162
|
+
console.log(` ${client.name.padEnd(22)} done`);
|
|
136
163
|
}
|
|
137
164
|
catch (err) {
|
|
138
|
-
console.log(` ${client.name.padEnd(
|
|
165
|
+
console.log(` ${client.name.padEnd(22)} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
139
166
|
}
|
|
140
167
|
});
|
|
141
168
|
console.log('');
|
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
|
}
|