@warmio/mcp 4.3.3 → 5.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/README.md +39 -25
- package/dist/index.js +11 -15
- package/dist/install.js +15 -30
- package/dist/warm-api-client.js +8 -10
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Warmio
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
This package starts in local `stdio` mode by default and also supports optional Streamable HTTP.
|
|
3
|
+
Warmio is the one-command CLI and read-only MCP server for Warm financial data.
|
|
6
4
|
|
|
7
5
|
## Install
|
|
8
6
|
|
|
@@ -10,17 +8,9 @@ This package starts in local `stdio` mode by default and also supports optional
|
|
|
10
8
|
npx -y @warmio/mcp@latest
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
The installer detects supported MCP clients, prompts for your Warm API key,
|
|
14
|
-
`stdio`
|
|
15
|
-
being duplicated into every MCP client config.
|
|
16
|
-
|
|
17
|
-
Use `@latest` when you want the newest published package. Bare `npx @warmio/mcp` can reuse a
|
|
18
|
-
cached or local copy and leave you on an older version. If you want a deterministic install,
|
|
19
|
-
replace `@latest` with an exact version such as `@warmio/mcp@4.3.3`.
|
|
20
|
-
|
|
21
|
-
When launched without arguments in a non-interactive context, the binary automatically starts the
|
|
22
|
-
`stdio` server instead of the installer. This keeps MCP clients that invoke the package directly
|
|
23
|
-
from falling into the interactive setup flow.
|
|
11
|
+
The installer detects supported MCP clients, prompts for your Warm API key, validates it, and
|
|
12
|
+
writes the local `stdio` MCP config automatically. The key is stored once in your local Warm
|
|
13
|
+
profile instead of being duplicated into every MCP client config.
|
|
24
14
|
|
|
25
15
|
## Requirements
|
|
26
16
|
|
|
@@ -28,41 +18,65 @@ from falling into the interactive setup flow.
|
|
|
28
18
|
- A Warm API key from [Settings -> API Keys](https://warm.io/settings)
|
|
29
19
|
- Node.js 18+
|
|
30
20
|
|
|
31
|
-
## Manual
|
|
21
|
+
## Manual MCP Config
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
stay secret-free:
|
|
23
|
+
Use this when a client asks you to paste an MCP JSON config:
|
|
35
24
|
|
|
36
25
|
```json
|
|
37
26
|
{
|
|
38
27
|
"mcpServers": {
|
|
39
28
|
"warm": {
|
|
40
29
|
"command": "npx",
|
|
41
|
-
"args": ["-y", "@warmio/mcp@latest", "
|
|
30
|
+
"args": ["-y", "@warmio/mcp@latest", "mcp"]
|
|
42
31
|
}
|
|
43
32
|
}
|
|
44
33
|
}
|
|
45
34
|
```
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
On Windows, prefer:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"warm": {
|
|
42
|
+
"command": "cmd",
|
|
43
|
+
"args": ["/c", "npx", "-y", "@warmio/mcp@latest", "mcp"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
49
48
|
|
|
50
49
|
Optional auth overrides:
|
|
51
50
|
|
|
52
51
|
- `WARM_API_KEY`
|
|
53
52
|
- `WARM_API_KEY_FILE`
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
## Universal copy/paste setup prompt
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
I want to connect Warm to this AI app using MCP.
|
|
58
|
+
|
|
59
|
+
Use this MCP server config:
|
|
56
60
|
|
|
57
|
-
```json
|
|
58
61
|
{
|
|
59
62
|
"mcpServers": {
|
|
60
63
|
"warm": {
|
|
61
|
-
"command": "
|
|
62
|
-
"args": ["
|
|
64
|
+
"command": "npx",
|
|
65
|
+
"args": ["-y", "@warmio/mcp@latest", "mcp"]
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
}
|
|
69
|
+
|
|
70
|
+
If this app supports MCP configuration, add a server named "warm" with that command.
|
|
71
|
+
|
|
72
|
+
Do not ask me to paste my Warm API key into this chat. Help me store it locally using Warm's setup
|
|
73
|
+
flow, then test the connection by calling Warm's verify_key tool.
|
|
74
|
+
|
|
75
|
+
After setup, I should be able to ask:
|
|
76
|
+
- What’s my net worth?
|
|
77
|
+
- Summarize my recent transactions.
|
|
78
|
+
- What changed recently?
|
|
79
|
+
- What recurring charges do I have?
|
|
66
80
|
```
|
|
67
81
|
|
|
68
82
|
## Optional Streamable HTTP
|
package/dist/index.js
CHANGED
|
@@ -9,24 +9,20 @@ function isInteractiveSession() {
|
|
|
9
9
|
}
|
|
10
10
|
function printUsage() {
|
|
11
11
|
console.log('');
|
|
12
|
-
console.log('
|
|
13
|
-
console.log('
|
|
12
|
+
console.log(' Warmio');
|
|
13
|
+
console.log(' ------');
|
|
14
14
|
console.log('');
|
|
15
|
-
console.log('
|
|
16
|
-
console.log('
|
|
17
|
-
console.log(' Non-interactive launch: starts stdio server');
|
|
15
|
+
console.log(' warmio');
|
|
16
|
+
console.log(' Run the interactive installer');
|
|
18
17
|
console.log('');
|
|
19
|
-
console.log('
|
|
20
|
-
console.log('
|
|
21
|
-
console.log('
|
|
22
|
-
console.log('');
|
|
23
|
-
console.log(' Aliases:');
|
|
24
|
-
console.log(' --server, --stdio Start stdio mode');
|
|
18
|
+
console.log(' warmio install [--force] [--no-validate]');
|
|
19
|
+
console.log(' warmio mcp');
|
|
20
|
+
console.log(' warmio http [--host 127.0.0.1] [--port 3000] [--path /mcp]');
|
|
25
21
|
console.log('');
|
|
26
22
|
}
|
|
27
23
|
function parseCliArgs(args) {
|
|
28
24
|
const options = {
|
|
29
|
-
command: isInteractiveSession() ? 'install' : '
|
|
25
|
+
command: isInteractiveSession() ? 'install' : 'mcp',
|
|
30
26
|
force: false,
|
|
31
27
|
validateApiKey: true,
|
|
32
28
|
};
|
|
@@ -40,8 +36,8 @@ function parseCliArgs(args) {
|
|
|
40
36
|
options.command = 'install';
|
|
41
37
|
continue;
|
|
42
38
|
}
|
|
43
|
-
if (arg === '
|
|
44
|
-
options.command = '
|
|
39
|
+
if (arg === 'mcp') {
|
|
40
|
+
options.command = 'mcp';
|
|
45
41
|
continue;
|
|
46
42
|
}
|
|
47
43
|
if (arg === 'http') {
|
|
@@ -96,7 +92,7 @@ async function main() {
|
|
|
96
92
|
case 'help':
|
|
97
93
|
printUsage();
|
|
98
94
|
return;
|
|
99
|
-
case '
|
|
95
|
+
case 'mcp':
|
|
100
96
|
await startStdioServer();
|
|
101
97
|
return;
|
|
102
98
|
case 'http':
|
package/dist/install.js
CHANGED
|
@@ -48,8 +48,8 @@ const GLOBAL_CLIENTS = [
|
|
|
48
48
|
const PROJECT_CONFIGS = ['.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json'];
|
|
49
49
|
const MCP_PACKAGE_SPEC = '@warmio/mcp@latest';
|
|
50
50
|
const MCP_CONFIG = platform() === 'win32'
|
|
51
|
-
? { command: 'cmd', args: ['/c', 'npx', '-y', MCP_PACKAGE_SPEC, '
|
|
52
|
-
: { command: 'npx', args: ['-y', MCP_PACKAGE_SPEC, '
|
|
51
|
+
? { command: 'cmd', args: ['/c', 'npx', '-y', MCP_PACKAGE_SPEC, 'mcp'] }
|
|
52
|
+
: { command: 'npx', args: ['-y', MCP_PACKAGE_SPEC, 'mcp'] };
|
|
53
53
|
const WARM_API_KEY_PATH = getWarmApiKeyPath();
|
|
54
54
|
const WARM_TOML_BLOCK_PATTERN = /\n?\[mcp_servers\.warm\][\s\S]*?(?=\n\[[^\n]+\]|\s*$)/g;
|
|
55
55
|
const WARM_TOML_ENV_BLOCK_PATTERN = /\n?\[mcp_servers\.warm\.env\][\s\S]*?(?=\n\[[^\n]+\]|\s*$)/g;
|
|
@@ -83,22 +83,11 @@ function getStringArray(value) {
|
|
|
83
83
|
function isWarmPackageSpecifier(value) {
|
|
84
84
|
return /^@warmio\/mcp(?:@.+)?$/.test(value);
|
|
85
85
|
}
|
|
86
|
-
function isLegacyWarmPackageSpecifier(value) {
|
|
87
|
-
return /^@anthropic\/warm-mcp-server(?:@.+)?$/.test(value);
|
|
88
|
-
}
|
|
89
|
-
function normalizePathLikeValue(value) {
|
|
90
|
-
return value.replace(/\\/g, '/').toLowerCase();
|
|
91
|
-
}
|
|
92
|
-
function isLocalWarmMcpPath(value) {
|
|
93
|
-
const normalized = normalizePathLikeValue(value);
|
|
94
|
-
return normalized.includes('/warm-mcp/dist/index.js') || normalized.endsWith('/warm-mcp');
|
|
95
|
-
}
|
|
96
86
|
function isSupportedWarmInvocation(command, args) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return [command, ...args].some((value) => typeof value === 'string' && isLegacyWarmPackageSpecifier(value));
|
|
87
|
+
if (command !== 'npx' && command !== 'cmd') {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return args.some((value) => isWarmPackageSpecifier(value)) && args.includes('mcp');
|
|
102
91
|
}
|
|
103
92
|
function getJsonEnvStatus(server) {
|
|
104
93
|
const env = isRecord(server?.env) ? server.env : undefined;
|
|
@@ -121,13 +110,10 @@ function getJsonStatus(client, content) {
|
|
|
121
110
|
}
|
|
122
111
|
const command = typeof server.command === 'string' ? server.command : undefined;
|
|
123
112
|
const args = getStringArray(server.args);
|
|
124
|
-
if (client.isProjectLevel && command && !isLegacyWarmInvocation(command, args)) {
|
|
125
|
-
return { client, state: 'configured', ...envStatus };
|
|
126
|
-
}
|
|
127
113
|
if (isSupportedWarmInvocation(command, args)) {
|
|
128
114
|
return { client, state: 'configured', ...envStatus };
|
|
129
115
|
}
|
|
130
|
-
return { client, state: '
|
|
116
|
+
return { client, state: 'needs_setup', ...envStatus };
|
|
131
117
|
}
|
|
132
118
|
catch {
|
|
133
119
|
return { client, state: 'missing', hasApiKeyFileOverride: false, inlineApiKey: null };
|
|
@@ -190,7 +176,7 @@ function getTomlStatus(client, content) {
|
|
|
190
176
|
if (isSupportedWarmInvocation(command, args)) {
|
|
191
177
|
return { client, state: 'configured', ...envStatus };
|
|
192
178
|
}
|
|
193
|
-
return { client, state: '
|
|
179
|
+
return { client, state: 'needs_setup', ...envStatus };
|
|
194
180
|
}
|
|
195
181
|
function getClientStatus(client) {
|
|
196
182
|
if (!existsSync(client.configPath)) {
|
|
@@ -223,8 +209,7 @@ function configureJson(client) {
|
|
|
223
209
|
const existingArgs = getStringArray(existing?.args);
|
|
224
210
|
const preserveProjectCommand = client.isProjectLevel &&
|
|
225
211
|
!!existingCommand &&
|
|
226
|
-
|
|
227
|
-
!isWarmPackageSpecifier(existingCommand);
|
|
212
|
+
isSupportedWarmInvocation(existingCommand, existingArgs);
|
|
228
213
|
delete nextEnv.WARM_API_KEY;
|
|
229
214
|
if (preserveProjectCommand) {
|
|
230
215
|
servers.warm = {
|
|
@@ -252,8 +237,8 @@ function configureToml(client) {
|
|
|
252
237
|
}
|
|
253
238
|
const tomlCommand = platform() === 'win32' ? 'cmd' : 'npx';
|
|
254
239
|
const tomlArgs = platform() === 'win32'
|
|
255
|
-
? `["/c", "npx", "-y", "${MCP_PACKAGE_SPEC}", "
|
|
256
|
-
: `["-y", "${MCP_PACKAGE_SPEC}", "
|
|
240
|
+
? `["/c", "npx", "-y", "${MCP_PACKAGE_SPEC}", "mcp"]`
|
|
241
|
+
: `["-y", "${MCP_PACKAGE_SPEC}", "mcp"]`;
|
|
257
242
|
const warmBlock = `[mcp_servers.warm]\ncommand = "${tomlCommand}"\nargs = ${tomlArgs}\n`;
|
|
258
243
|
const preservedEnvLines = getTomlEnvStatus(content).envLines.filter((line) => !/^\s*WARM_API_KEY\s*=/.test(line));
|
|
259
244
|
let nextContent = content
|
|
@@ -332,8 +317,8 @@ function getStatusLabel(status, force) {
|
|
|
332
317
|
if (status === 'configured') {
|
|
333
318
|
return 'configured';
|
|
334
319
|
}
|
|
335
|
-
if (status === '
|
|
336
|
-
return 'needs
|
|
320
|
+
if (status === 'needs_setup') {
|
|
321
|
+
return 'needs setup';
|
|
337
322
|
}
|
|
338
323
|
return 'not configured';
|
|
339
324
|
}
|
|
@@ -341,8 +326,8 @@ export async function install(options = {}) {
|
|
|
341
326
|
const force = options.force ?? false;
|
|
342
327
|
const shouldValidateApiKey = options.validateApiKey ?? true;
|
|
343
328
|
console.log('');
|
|
344
|
-
console.log('
|
|
345
|
-
console.log('
|
|
329
|
+
console.log(' Warmio Installer');
|
|
330
|
+
console.log(' ----------------');
|
|
346
331
|
console.log('');
|
|
347
332
|
const globalClients = GLOBAL_CLIENTS.filter(isDetected);
|
|
348
333
|
const projectClients = detectProjectClients();
|
package/dist/warm-api-client.js
CHANGED
|
@@ -4,7 +4,6 @@ const DEFAULT_REQUEST_TIMEOUT_MS = (() => {
|
|
|
4
4
|
const raw = Number(process.env.WARM_API_TIMEOUT_MS || 10_000);
|
|
5
5
|
return Number.isFinite(raw) && raw > 0 ? raw : 10_000;
|
|
6
6
|
})();
|
|
7
|
-
const DEFAULT_SPENDING_MONTHS = 6;
|
|
8
7
|
let cachedApiKey;
|
|
9
8
|
function roundMoney(value) {
|
|
10
9
|
return Math.round(value * 100) / 100;
|
|
@@ -56,6 +55,8 @@ function normalizeAccounts(accounts) {
|
|
|
56
55
|
};
|
|
57
56
|
}
|
|
58
57
|
function normalizeFinancialState(input) {
|
|
58
|
+
const latestSnapshot = input.snapshots?.[0];
|
|
59
|
+
const categorySpending = latestSnapshot?.spending_by_category ?? [];
|
|
59
60
|
return {
|
|
60
61
|
budgets: (input.budgets || []).map((budget) => ({
|
|
61
62
|
amount: budget.amount ?? 0,
|
|
@@ -66,8 +67,8 @@ function normalizeFinancialState(input) {
|
|
|
66
67
|
spent: budget.spent ?? 0,
|
|
67
68
|
status: budget.status ?? null,
|
|
68
69
|
})),
|
|
69
|
-
category_spending:
|
|
70
|
-
amount: roundMoney(item?.total ?? 0),
|
|
70
|
+
category_spending: categorySpending.map((item) => ({
|
|
71
|
+
amount: roundMoney(item?.amount ?? item?.total ?? 0),
|
|
71
72
|
category: item?.category || 'Uncategorized',
|
|
72
73
|
})),
|
|
73
74
|
generated_at: input.generatedAt,
|
|
@@ -163,7 +164,7 @@ export async function apiRequest(endpoint, params = {}, options = {}) {
|
|
|
163
164
|
const requestOptions = createWarmApiClientConfig(options);
|
|
164
165
|
const apiKey = requestOptions.apiKeyResolver?.() ?? getConfiguredApiKey();
|
|
165
166
|
if (!apiKey) {
|
|
166
|
-
throw new Error('WARM_API_KEY not set. Run "npx
|
|
167
|
+
throw new Error('WARM_API_KEY not set. Run "npx @warmio/mcp" to configure or set WARM_API_KEY.');
|
|
167
168
|
}
|
|
168
169
|
const url = new URL(endpoint, requestOptions.apiUrl);
|
|
169
170
|
Object.entries(params).forEach(([key, value]) => {
|
|
@@ -193,7 +194,7 @@ export async function apiRequest(endpoint, params = {}, options = {}) {
|
|
|
193
194
|
if (!response.ok) {
|
|
194
195
|
const errorMessages = {
|
|
195
196
|
401: 'Invalid or expired API key. Regenerate at https://warm.io/settings',
|
|
196
|
-
403: '
|
|
197
|
+
403: 'API access is available on paid plans only. Upgrade at https://warm.io/settings',
|
|
197
198
|
429: 'Rate limit exceeded. Try again in a few minutes.',
|
|
198
199
|
};
|
|
199
200
|
if (errorMessages[response.status]) {
|
|
@@ -219,7 +220,7 @@ export function createWarmApiClient(options = {}) {
|
|
|
219
220
|
return normalizeAccounts(response.accounts || []);
|
|
220
221
|
};
|
|
221
222
|
const getFinancialState = async () => {
|
|
222
|
-
const [snapshotsResponse, recurringResponse, budgetsResponse, goalsResponse, healthResponse, liabilitiesResponse, holdingsResponse,
|
|
223
|
+
const [snapshotsResponse, recurringResponse, budgetsResponse, goalsResponse, healthResponse, liabilitiesResponse, holdingsResponse,] = await Promise.all([
|
|
223
224
|
apiRequest('/api/export', { dataset: 'snapshots' }, options),
|
|
224
225
|
apiRequest('/api/export', { dataset: 'recurring' }, options),
|
|
225
226
|
apiRequest('/api/export', { dataset: 'budgets' }, options),
|
|
@@ -227,10 +228,8 @@ export function createWarmApiClient(options = {}) {
|
|
|
227
228
|
apiRequest('/api/export', { dataset: 'health' }, options),
|
|
228
229
|
apiRequest('/api/export', { dataset: 'liabilities' }, options),
|
|
229
230
|
apiRequest('/api/export', { dataset: 'holdings' }, options),
|
|
230
|
-
apiRequest('/api/spending', { months: String(DEFAULT_SPENDING_MONTHS) }, options),
|
|
231
231
|
]);
|
|
232
|
-
const generatedAt =
|
|
233
|
-
holdingsResponse.generated_at ||
|
|
232
|
+
const generatedAt = holdingsResponse.generated_at ||
|
|
234
233
|
liabilitiesResponse.generated_at ||
|
|
235
234
|
healthResponse.generated_at ||
|
|
236
235
|
goalsResponse.generated_at ||
|
|
@@ -240,7 +239,6 @@ export function createWarmApiClient(options = {}) {
|
|
|
240
239
|
new Date().toISOString();
|
|
241
240
|
return normalizeFinancialState({
|
|
242
241
|
budgets: budgetsResponse.budgets,
|
|
243
|
-
categorySpending: spendingResponse.spending,
|
|
244
242
|
generatedAt,
|
|
245
243
|
goals: goalsResponse.goals,
|
|
246
244
|
health: healthResponse,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warmio/mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Warm MCP server for
|
|
3
|
+
"version": "5.0.1",
|
|
4
|
+
"description": "Warm CLI and MCP server for personal financial data",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -27,24 +27,28 @@
|
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"bin": {
|
|
30
|
-
"
|
|
30
|
+
"warmio": "dist/index.js"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"dist"
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsc",
|
|
37
|
-
"
|
|
37
|
+
"test": "npm run build && node --test test/*.mjs",
|
|
38
|
+
"start": "node dist/index.js mcp",
|
|
38
39
|
"start:http": "node dist/index.js http",
|
|
39
40
|
"start:install": "node dist/index.js install",
|
|
40
|
-
"prepublishOnly": "npm run build"
|
|
41
|
+
"prepublishOnly": "npm run build",
|
|
42
|
+
"start:mcp": "node dist/index.js mcp"
|
|
41
43
|
},
|
|
42
44
|
"keywords": [
|
|
43
45
|
"mcp",
|
|
44
46
|
"warm",
|
|
47
|
+
"warmio",
|
|
45
48
|
"financial",
|
|
46
49
|
"api",
|
|
47
|
-
"claude"
|
|
50
|
+
"claude",
|
|
51
|
+
"chatgpt"
|
|
48
52
|
],
|
|
49
53
|
"license": "MIT",
|
|
50
54
|
"dependencies": {
|