@warmio/mcp 3.0.2 → 4.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/README.md CHANGED
@@ -1,60 +1,293 @@
1
- # Warm MCP Server
1
+ # Warm MCP
2
2
 
3
- MCP server that gives Claude Code (or any MCP client) read-only access to your Warm financial data.
3
+ Read-only MCP server for Warm financial data.
4
4
 
5
- ## Quick Start
5
+ Warm supports two transport shapes from this repo:
6
+
7
+ - Local `stdio` via the npm package `@warmio/mcp`
8
+ - Self-hosted Streamable HTTP via `warm-mcp http`
9
+
10
+ Warm does not currently publish a Warm-hosted Streamable HTTP MCP endpoint from this repo.
11
+
12
+ ## Install
6
13
 
7
14
  ```bash
8
15
  npx @warmio/mcp
9
16
  ```
10
17
 
11
- Detects installed MCP clients, prompts for your API key, and configures everything automatically.
12
- Works on macOS, Linux, and Windows. Supports: Claude Code, Claude Desktop, Cursor, Windsurf, OpenCode, Codex CLI, Antigravity, Gemini CLI.
18
+ The installer detects supported MCP clients, prompts for your Warm API key, and writes the local
19
+ `stdio` server config automatically. The key is stored once in your local Warm profile instead of
20
+ being duplicated into every MCP client config.
13
21
 
14
- After setup, open your MCP client and ask:
15
- - "What's my net worth?"
16
- - "How much did I spend on restaurants last month?"
17
- - "Show me my subscriptions"
22
+ ## Requirements
18
23
 
19
- ## Options
24
+ - Warm Pro
25
+ - A Warm API key from [Settings -> API Keys](https://warm.io/settings)
26
+ - Node.js 18+
20
27
 
21
- | Command | Description |
22
- |---------|-------------|
23
- | `npx @warmio/mcp` | Run the installer / configurator |
24
- | `npx @warmio/mcp --force` | Re-run installer (updates API key in all configs) |
25
- | `npx @warmio/mcp --server` | Start the MCP server (used internally by clients) |
28
+ ## Manual `stdio` Config
26
29
 
27
- ## Requirements
30
+ The installer stores your API key in the Warm config directory and generated MCP client configs can
31
+ stay secret-free:
28
32
 
29
- - **Pro subscription** — API access requires Warm Pro
30
- - **API key** — Generate in [Settings → API Keys](https://warm.io/settings)
31
- - **Node.js 18+** — For running the MCP server
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "warm": {
37
+ "command": "npx",
38
+ "args": ["-y", "@warmio/mcp", "--server"]
39
+ }
40
+ }
41
+ }
42
+ ```
32
43
 
33
- ## How It Works
44
+ Optional auth overrides:
34
45
 
35
- 1. You run `npx @warmio/mcp` once
36
- 2. It detects which MCP clients you have installed
37
- 3. Prompts for your Warm API key
38
- 4. Writes the server config into each client's settings
39
- 5. Each client is configured to run `npx -y @warmio/mcp --server` on demand
46
+ - `WARM_API_KEY`
47
+ - `WARM_API_KEY_FILE`
40
48
 
41
- The MCP server starts automatically when your client needs it — you never run it manually.
49
+ ## Self-hosted Streamable HTTP
42
50
 
43
- ## Available Tools
51
+ Run the HTTP server locally or behind your own reverse proxy:
44
52
 
45
- | Tool | Description |
46
- |------|-------------|
47
- | `get_accounts` | List all connected bank accounts with balances |
48
- | `get_transactions` | Get transactions with date/limit filters |
49
- | `get_recurring` | Show subscriptions and recurring payments |
50
- | `get_snapshots` | Net worth history (daily or monthly) |
51
- | `verify_key` | Check if API key is valid |
53
+ ```bash
54
+ npx @warmio/mcp http --host 127.0.0.1 --port 3000 --path /mcp
55
+ ```
56
+
57
+ Environment overrides:
58
+
59
+ - `WARM_MCP_HTTP_HOST`
60
+ - `WARM_MCP_HTTP_PORT`
61
+ - `WARM_MCP_HTTP_PATH`
62
+ - `WARM_MCP_ALLOWED_HOSTS`
63
+ - `WARM_API_KEY_FILE`
64
+
65
+ On Windows, prefer:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "warm": {
71
+ "command": "cmd",
72
+ "args": ["/c", "npx", "-y", "@warmio/mcp", "--server"]
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ ## Core Tools
79
+
80
+ Warm's published/documented MCP surface is the following four-tool core:
81
+
82
+ | Tool | Description |
83
+ | --------------------- | ----------------------------------------------- |
84
+ | `get_accounts` | List connected accounts with current balances |
85
+ | `get_transactions` | Page through transactions with an opaque cursor |
86
+ | `get_financial_state` | Return the current typed financial state bundle |
87
+ | `verify_key` | Validate the configured API key |
88
+
89
+ ## Strict Contract
90
+
91
+ - Every tool takes a JSON object input and returns a JSON object output.
92
+ - Treat the contracts as closed and typed. Do not depend on undocumented fields.
93
+ - Calendar dates use `YYYY-MM-DD`. Incremental sync timestamps use ISO 8601 datetimes.
94
+ - Amounts are numbers, never formatted strings.
95
+ - Transaction amounts follow the Plaid sign convention:
96
+ positive = expense/debit, negative = income/credit.
97
+ - Pagination cursors are opaque strings. Do not parse them or mix them with changed filters.
98
+
99
+ ### `get_accounts`
100
+
101
+ Input:
102
+
103
+ ```json
104
+ {}
105
+ ```
106
+
107
+ Returns:
108
+
109
+ ```json
110
+ {
111
+ "accounts": [
112
+ {
113
+ "name": "Primary Checking",
114
+ "type": "depository",
115
+ "subtype": "checking",
116
+ "balance": 2450.12,
117
+ "institution": "Chase",
118
+ "mask": "1234"
119
+ }
120
+ ]
121
+ }
122
+ ```
123
+
124
+ ### `get_transactions`
125
+
126
+ Input:
127
+
128
+ ```json
129
+ {
130
+ "limit": 100,
131
+ "cursor": "opaque-cursor-from-a-prior-page",
132
+ "last_knowledge": "2026-03-11T00:00:00.000Z",
133
+ "search": "coffee"
134
+ }
135
+ ```
136
+
137
+ Returns:
138
+
139
+ ```json
140
+ {
141
+ "generated_at": "2026-03-11T12:00:00.000Z",
142
+ "next_knowledge": "2026-03-11T12:00:00.000Z",
143
+ "txns": [
144
+ {
145
+ "id": "txn_123",
146
+ "date": "2026-01-15",
147
+ "amount": 12.34,
148
+ "merchant": "Coffee Shop",
149
+ "description": "COFFEE SHOP",
150
+ "category": "FOOD_AND_DRINK",
151
+ "detailed_category": "FOOD_AND_DRINK_COFFEE"
152
+ }
153
+ ],
154
+ "pagination": {
155
+ "limit": 100,
156
+ "next_cursor": "opaque-next-cursor",
157
+ "has_more": true
158
+ }
159
+ }
160
+ ```
161
+
162
+ Cursor model:
163
+
164
+ 1. Omit `cursor` on the first call.
165
+ 2. Keep `limit` and any filters such as `search` fixed while following a cursor chain.
166
+ 3. If `pagination.next_cursor` is non-null, pass it unchanged to fetch the next page.
167
+ 4. Stop when `next_cursor` is `null`.
168
+ 5. Do not combine `cursor` with `last_knowledge`.
169
+
170
+ ### `get_financial_state`
171
+
172
+ Input:
173
+
174
+ ```json
175
+ {}
176
+ ```
177
+
178
+ Returns:
179
+
180
+ ```json
181
+ {
182
+ "generated_at": "2026-03-11T12:00:00.000Z",
183
+ "snapshots": [
184
+ {
185
+ "date": "2026-03-11",
186
+ "net_worth": 125430.55,
187
+ "total_assets": 168210.77,
188
+ "total_liabilities": 42780.22
189
+ }
190
+ ],
191
+ "recurring": [
192
+ {
193
+ "merchant": "Netflix",
194
+ "amount": 15.49,
195
+ "frequency": "MONTHLY",
196
+ "next_date": "2026-03-18",
197
+ "type": "subscription",
198
+ "active": true
199
+ }
200
+ ],
201
+ "budgets": [
202
+ {
203
+ "name": "Dining Out",
204
+ "amount": 400,
205
+ "spent": 182.55,
206
+ "remaining": 217.45,
207
+ "percent_used": 45.64,
208
+ "period": "monthly",
209
+ "status": "on_track"
210
+ }
211
+ ],
212
+ "goals": [
213
+ {
214
+ "name": "Emergency Fund",
215
+ "target": 10000,
216
+ "current": 4200,
217
+ "progress_percent": 42,
218
+ "target_date": null,
219
+ "status": "active",
220
+ "category": "safety",
221
+ "monthly_contribution_needed": 400
222
+ }
223
+ ],
224
+ "health": {
225
+ "score": 78,
226
+ "label": "Good",
227
+ "data_completeness": 94,
228
+ "pillars": {
229
+ "spend": 20,
230
+ "save": 23,
231
+ "borrow": 15,
232
+ "build": 20
233
+ },
234
+ "message": null
235
+ },
236
+ "liabilities": [
237
+ {
238
+ "account_id": "acc_loan_1",
239
+ "type": "student",
240
+ "balance": 12450.22,
241
+ "apr_percentage": 5.2,
242
+ "minimum_payment": 145,
243
+ "next_payment_due_date": "2026-03-22",
244
+ "is_overdue": false
245
+ }
246
+ ],
247
+ "holdings": [
248
+ {
249
+ "account_id": "acc_inv_1",
250
+ "security_name": "Vanguard Total Stock Market ETF",
251
+ "symbol": "VTI",
252
+ "type": "etf",
253
+ "quantity": 12.5,
254
+ "value": 3541.25,
255
+ "cost_basis": 3010
256
+ }
257
+ ],
258
+ "category_spending": [
259
+ {
260
+ "category": "FOOD_AND_DRINK",
261
+ "amount": 182.55
262
+ }
263
+ ]
264
+ }
265
+ ```
266
+
267
+ If Warm does not have enough state data yet, nullable fields remain `null`.
268
+
269
+ ### `verify_key`
270
+
271
+ Input:
272
+
273
+ ```json
274
+ {}
275
+ ```
276
+
277
+ Returns:
278
+
279
+ ```json
280
+ {
281
+ "valid": true,
282
+ "status": "ok"
283
+ }
284
+ ```
52
285
 
53
286
  ## Security
54
287
 
55
- - **Read-only** Cannot modify, delete, or transfer data
56
- - **Scoped** Key only accesses your accounts
57
- - **Revocable** Delete key in Settings to revoke instantly
288
+ - Read-only: no write, delete, transfer, or mutation tools
289
+ - Scoped: the key only reads the owner's Warm data
290
+ - Revocable: delete the key in Settings to revoke access immediately
58
291
 
59
292
  ## Development
60
293
 
@@ -0,0 +1,2 @@
1
+ export declare function getWarmConfigDir(): string;
2
+ export declare function getWarmApiKeyPath(): string;
@@ -0,0 +1,23 @@
1
+ import * as os from 'node:os';
2
+ import * as path from 'node:path';
3
+ export function getWarmConfigDir() {
4
+ if (process.env.WARM_CONFIG_DIR?.trim()) {
5
+ return process.env.WARM_CONFIG_DIR.trim();
6
+ }
7
+ if (process.platform === 'win32') {
8
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Warm');
9
+ }
10
+ if (process.env.XDG_CONFIG_HOME?.trim()) {
11
+ return path.join(process.env.XDG_CONFIG_HOME.trim(), 'warm');
12
+ }
13
+ if (process.platform === 'darwin') {
14
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Warm');
15
+ }
16
+ return path.join(os.homedir(), '.config', 'warm');
17
+ }
18
+ export function getWarmApiKeyPath() {
19
+ if (process.env.WARM_API_KEY_FILE?.trim()) {
20
+ return process.env.WARM_API_KEY_FILE.trim();
21
+ }
22
+ return path.join(getWarmConfigDir(), 'api_key');
23
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { Server as HttpServer } from 'http';
2
+ export interface WarmHttpServerOptions {
3
+ host?: string;
4
+ port?: number;
5
+ path?: string;
6
+ allowedHosts?: string[];
7
+ }
8
+ export declare function resolveHttpServerOptions(overrides?: WarmHttpServerOptions): Required<WarmHttpServerOptions>;
9
+ export declare function startHttpServer(options?: WarmHttpServerOptions): Promise<HttpServer>;
package/dist/http.js ADDED
@@ -0,0 +1,117 @@
1
+ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import { createWarmServer } from './server.js';
4
+ const DEFAULT_HTTP_HOST = '127.0.0.1';
5
+ const DEFAULT_HTTP_PORT = 3000;
6
+ const DEFAULT_HTTP_PATH = '/mcp';
7
+ function parsePort(value, fallback) {
8
+ const parsed = Number(value);
9
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
10
+ }
11
+ function normalizePath(value) {
12
+ if (!value) {
13
+ return DEFAULT_HTTP_PATH;
14
+ }
15
+ return value.startsWith('/') ? value : `/${value}`;
16
+ }
17
+ function parseAllowedHosts(value) {
18
+ if (!value) {
19
+ return [];
20
+ }
21
+ return value
22
+ .split(',')
23
+ .map((host) => host.trim())
24
+ .filter(Boolean);
25
+ }
26
+ function jsonRpcError(message) {
27
+ return {
28
+ jsonrpc: '2.0',
29
+ error: {
30
+ code: -32000,
31
+ message,
32
+ },
33
+ id: null,
34
+ };
35
+ }
36
+ function sendMethodNotAllowed(res) {
37
+ res.status(405).set('Allow', 'POST').json(jsonRpcError('Method not allowed.'));
38
+ }
39
+ export function resolveHttpServerOptions(overrides = {}) {
40
+ return {
41
+ host: overrides.host ||
42
+ process.env.WARM_MCP_HTTP_HOST ||
43
+ process.env.MCP_HOST ||
44
+ process.env.HOST ||
45
+ DEFAULT_HTTP_HOST,
46
+ port: overrides.port ??
47
+ parsePort(process.env.WARM_MCP_HTTP_PORT || process.env.MCP_PORT || process.env.PORT, DEFAULT_HTTP_PORT),
48
+ path: normalizePath(overrides.path || process.env.WARM_MCP_HTTP_PATH || process.env.MCP_PATH || DEFAULT_HTTP_PATH),
49
+ allowedHosts: overrides.allowedHosts ??
50
+ parseAllowedHosts(process.env.WARM_MCP_ALLOWED_HOSTS || process.env.MCP_ALLOWED_HOSTS),
51
+ };
52
+ }
53
+ export async function startHttpServer(options = {}) {
54
+ const resolved = resolveHttpServerOptions(options);
55
+ const app = createMcpExpressApp({
56
+ host: resolved.host,
57
+ ...(resolved.allowedHosts.length > 0 ? { allowedHosts: resolved.allowedHosts } : {}),
58
+ });
59
+ app.post(resolved.path, async (req, res) => {
60
+ const server = createWarmServer();
61
+ const transport = new StreamableHTTPServerTransport({
62
+ sessionIdGenerator: undefined,
63
+ });
64
+ let cleanedUp = false;
65
+ const cleanup = async () => {
66
+ if (cleanedUp) {
67
+ return;
68
+ }
69
+ cleanedUp = true;
70
+ await Promise.allSettled([transport.close(), server.close()]);
71
+ };
72
+ res.on('close', () => {
73
+ void cleanup();
74
+ });
75
+ try {
76
+ await server.connect(transport);
77
+ await transport.handleRequest(req, res, req.body);
78
+ }
79
+ catch (error) {
80
+ console.error('Error handling Warm MCP HTTP request:', error);
81
+ if (!res.headersSent) {
82
+ res.status(500).json(jsonRpcError('Internal server error'));
83
+ }
84
+ void cleanup();
85
+ }
86
+ });
87
+ app.get(resolved.path, (_req, res) => {
88
+ sendMethodNotAllowed(res);
89
+ });
90
+ app.delete(resolved.path, (_req, res) => {
91
+ sendMethodNotAllowed(res);
92
+ });
93
+ const listener = await new Promise((resolve, reject) => {
94
+ const httpServer = app.listen(resolved.port, resolved.host, () => {
95
+ resolve(httpServer);
96
+ });
97
+ httpServer.once('error', reject);
98
+ });
99
+ let shuttingDown = false;
100
+ const shutdown = (signal) => {
101
+ if (shuttingDown) {
102
+ return;
103
+ }
104
+ shuttingDown = true;
105
+ console.log(`Received ${signal}, shutting down Warm MCP HTTP server...`);
106
+ listener.close((error) => {
107
+ if (error) {
108
+ console.error('Failed to close Warm MCP HTTP server:', error);
109
+ process.exitCode = 1;
110
+ }
111
+ });
112
+ };
113
+ process.once('SIGINT', () => shutdown('SIGINT'));
114
+ process.once('SIGTERM', () => shutdown('SIGTERM'));
115
+ console.log(`Warm MCP Streamable HTTP server listening on http://${resolved.host}:${resolved.port}${resolved.path}`);
116
+ return listener;
117
+ }
package/dist/index.js CHANGED
@@ -1,10 +1,143 @@
1
1
  #!/usr/bin/env node
2
- const args = process.argv.slice(2);
3
- if (args.includes('--server')) {
4
- await import('./server.js');
2
+ import { startHttpServer } from './http.js';
3
+ import { install } from './install.js';
4
+ import { startStdioServer } from './server.js';
5
+ function printUsage() {
6
+ console.log('');
7
+ console.log(' Warm MCP');
8
+ console.log(' --------');
9
+ console.log('');
10
+ console.log(' warm-mcp [install] [--force] [--no-validate]');
11
+ console.log(' warm-mcp stdio');
12
+ console.log(' warm-mcp http [--host 127.0.0.1] [--port 3000] [--path /mcp]');
13
+ console.log(' [--allowed-hosts host1,host2]');
14
+ console.log('');
15
+ console.log(' Aliases:');
16
+ console.log(' --server, --stdio Start stdio mode');
17
+ console.log(' --http Start HTTP mode');
18
+ console.log('');
5
19
  }
6
- else {
7
- const { install } = await import('./install.js');
8
- await install();
20
+ function parsePort(value) {
21
+ const parsed = Number(value);
22
+ if (!Number.isInteger(parsed) || parsed <= 0) {
23
+ throw new Error(`Invalid port: ${value}`);
24
+ }
25
+ return parsed;
26
+ }
27
+ function parseList(value) {
28
+ return value
29
+ .split(',')
30
+ .map((item) => item.trim())
31
+ .filter(Boolean);
32
+ }
33
+ function readOption(args, index, flag) {
34
+ const arg = args[index];
35
+ if (arg.startsWith(`${flag}=`)) {
36
+ return {
37
+ nextIndex: index,
38
+ value: arg.slice(flag.length + 1),
39
+ };
40
+ }
41
+ const value = args[index + 1];
42
+ if (!value) {
43
+ throw new Error(`Missing value for ${flag}`);
44
+ }
45
+ return {
46
+ nextIndex: index + 1,
47
+ value,
48
+ };
49
+ }
50
+ function parseCliArgs(args) {
51
+ const options = {
52
+ command: 'install',
53
+ force: false,
54
+ validateApiKey: true,
55
+ };
56
+ for (let index = 0; index < args.length; index += 1) {
57
+ const arg = args[index];
58
+ if (arg === 'help' || arg === '--help' || arg === '-h') {
59
+ options.command = 'help';
60
+ continue;
61
+ }
62
+ if (arg === 'install' || arg === '--install') {
63
+ options.command = 'install';
64
+ continue;
65
+ }
66
+ if (arg === 'stdio' || arg === 'server' || arg === '--stdio' || arg === '--server') {
67
+ options.command = 'stdio';
68
+ continue;
69
+ }
70
+ if (arg === 'http' || arg === '--http') {
71
+ options.command = 'http';
72
+ continue;
73
+ }
74
+ if (arg === '--force') {
75
+ options.force = true;
76
+ continue;
77
+ }
78
+ if (arg === '--no-validate') {
79
+ options.validateApiKey = false;
80
+ continue;
81
+ }
82
+ if (arg === '--host' || arg.startsWith('--host=')) {
83
+ const { nextIndex, value } = readOption(args, index, '--host');
84
+ options.host = value;
85
+ index = nextIndex;
86
+ continue;
87
+ }
88
+ if (arg === '--port' || arg.startsWith('--port=')) {
89
+ const { nextIndex, value } = readOption(args, index, '--port');
90
+ options.port = parsePort(value);
91
+ index = nextIndex;
92
+ continue;
93
+ }
94
+ if (arg === '--path' || arg.startsWith('--path=')) {
95
+ const { nextIndex, value } = readOption(args, index, '--path');
96
+ options.path = value;
97
+ index = nextIndex;
98
+ continue;
99
+ }
100
+ if (arg === '--allowed-hosts' || arg.startsWith('--allowed-hosts=')) {
101
+ const { nextIndex, value } = readOption(args, index, '--allowed-hosts');
102
+ options.allowedHosts = parseList(value);
103
+ index = nextIndex;
104
+ continue;
105
+ }
106
+ throw new Error(`Unknown argument: ${arg}`);
107
+ }
108
+ return options;
109
+ }
110
+ async function main() {
111
+ const options = parseCliArgs(process.argv.slice(2));
112
+ switch (options.command) {
113
+ case 'help':
114
+ printUsage();
115
+ return;
116
+ case 'http':
117
+ await startHttpServer({
118
+ allowedHosts: options.allowedHosts,
119
+ host: options.host,
120
+ path: options.path,
121
+ port: options.port,
122
+ });
123
+ return;
124
+ case 'stdio':
125
+ await startStdioServer();
126
+ return;
127
+ case 'install':
128
+ await install({
129
+ force: options.force,
130
+ validateApiKey: options.validateApiKey,
131
+ });
132
+ return;
133
+ }
134
+ }
135
+ try {
136
+ await main();
137
+ }
138
+ catch (error) {
139
+ const message = error instanceof Error ? error.message : String(error);
140
+ console.error(message);
141
+ console.error('Run "warm-mcp --help" for usage.');
142
+ process.exit(1);
9
143
  }
10
- export {};
package/dist/install.d.ts CHANGED
@@ -1 +1,5 @@
1
- export declare function install(): Promise<void>;
1
+ export interface InstallOptions {
2
+ force?: boolean;
3
+ validateApiKey?: boolean;
4
+ }
5
+ export declare function install(options?: InstallOptions): Promise<void>;