@warmio/mcp 4.1.0 → 4.2.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 +47 -38
- package/dist/config-paths.d.ts +2 -0
- package/dist/config-paths.js +23 -0
- package/dist/http.js +1 -1
- package/dist/index.js +0 -70
- package/dist/install.js +24 -10
- package/dist/schemas.d.ts +1 -0
- package/dist/schemas.js +8 -10
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/warm-server.js +4 -117
- package/package.json +1 -6
package/README.md
CHANGED
|
@@ -2,12 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Read-only MCP server for Warm financial data.
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
5
|
+
This package supports local `stdio` transport only.
|
|
11
6
|
|
|
12
7
|
## Install
|
|
13
8
|
|
|
@@ -16,7 +11,8 @@ npx @warmio/mcp
|
|
|
16
11
|
```
|
|
17
12
|
|
|
18
13
|
The installer detects supported MCP clients, prompts for your Warm API key, and writes the local
|
|
19
|
-
`stdio` server config automatically.
|
|
14
|
+
`stdio` server config automatically. The key is stored once in your local Warm profile instead of
|
|
15
|
+
being duplicated into every MCP client config.
|
|
20
16
|
|
|
21
17
|
## Requirements
|
|
22
18
|
|
|
@@ -26,36 +22,24 @@ The installer detects supported MCP clients, prompts for your Warm API key, and
|
|
|
26
22
|
|
|
27
23
|
## Manual `stdio` Config
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
The installer stores your API key in the Warm config directory and generated MCP client configs can
|
|
26
|
+
stay secret-free:
|
|
30
27
|
|
|
31
28
|
```json
|
|
32
29
|
{
|
|
33
30
|
"mcpServers": {
|
|
34
31
|
"warm": {
|
|
35
32
|
"command": "npx",
|
|
36
|
-
"args": ["-y", "@warmio/mcp", "--server"]
|
|
37
|
-
"env": {
|
|
38
|
-
"WARM_API_KEY": "your_warm_api_key"
|
|
39
|
-
}
|
|
33
|
+
"args": ["-y", "@warmio/mcp", "--server"]
|
|
40
34
|
}
|
|
41
35
|
}
|
|
42
36
|
}
|
|
43
37
|
```
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Run the HTTP server locally or behind your own reverse proxy:
|
|
39
|
+
Optional auth overrides:
|
|
48
40
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Environment overrides:
|
|
54
|
-
|
|
55
|
-
- `WARM_MCP_HTTP_HOST`
|
|
56
|
-
- `WARM_MCP_HTTP_PORT`
|
|
57
|
-
- `WARM_MCP_HTTP_PATH`
|
|
58
|
-
- `WARM_MCP_ALLOWED_HOSTS`
|
|
41
|
+
- `WARM_API_KEY`
|
|
42
|
+
- `WARM_API_KEY_FILE`
|
|
59
43
|
|
|
60
44
|
On Windows, prefer:
|
|
61
45
|
|
|
@@ -64,10 +48,7 @@ On Windows, prefer:
|
|
|
64
48
|
"mcpServers": {
|
|
65
49
|
"warm": {
|
|
66
50
|
"command": "cmd",
|
|
67
|
-
"args": ["/c", "npx", "-y", "@warmio/mcp", "--server"]
|
|
68
|
-
"env": {
|
|
69
|
-
"WARM_API_KEY": "your_warm_api_key"
|
|
70
|
-
}
|
|
51
|
+
"args": ["/c", "npx", "-y", "@warmio/mcp", "--server"]
|
|
71
52
|
}
|
|
72
53
|
}
|
|
73
54
|
}
|
|
@@ -77,12 +58,12 @@ On Windows, prefer:
|
|
|
77
58
|
|
|
78
59
|
Warm's published/documented MCP surface is the following four-tool core:
|
|
79
60
|
|
|
80
|
-
| Tool
|
|
81
|
-
|
|
82
|
-
| `get_accounts`
|
|
83
|
-
| `get_transactions`
|
|
61
|
+
| Tool | Description |
|
|
62
|
+
| --------------------- | ----------------------------------------------- |
|
|
63
|
+
| `get_accounts` | List connected accounts with current balances |
|
|
64
|
+
| `get_transactions` | Page through transactions with an opaque cursor |
|
|
84
65
|
| `get_financial_state` | Return the current typed financial state bundle |
|
|
85
|
-
| `verify_key`
|
|
66
|
+
| `verify_key` | Validate the configured API key |
|
|
86
67
|
|
|
87
68
|
## Strict Contract
|
|
88
69
|
|
|
@@ -127,7 +108,8 @@ Input:
|
|
|
127
108
|
{
|
|
128
109
|
"limit": 100,
|
|
129
110
|
"cursor": "opaque-cursor-from-a-prior-page",
|
|
130
|
-
"last_knowledge": "2026-03-11T00:00:00.000Z"
|
|
111
|
+
"last_knowledge": "2026-03-11T00:00:00.000Z",
|
|
112
|
+
"search": "coffee"
|
|
131
113
|
}
|
|
132
114
|
```
|
|
133
115
|
|
|
@@ -159,7 +141,7 @@ Returns:
|
|
|
159
141
|
Cursor model:
|
|
160
142
|
|
|
161
143
|
1. Omit `cursor` on the first call.
|
|
162
|
-
2. Keep `limit` fixed while following a cursor chain.
|
|
144
|
+
2. Keep `limit` and any filters such as `search` fixed while following a cursor chain.
|
|
163
145
|
3. If `pagination.next_cursor` is non-null, pass it unchanged to fetch the next page.
|
|
164
146
|
4. Stop when `next_cursor` is `null`.
|
|
165
147
|
5. Do not combine `cursor` with `last_knowledge`.
|
|
@@ -229,7 +211,35 @@ Returns:
|
|
|
229
211
|
"build": 20
|
|
230
212
|
},
|
|
231
213
|
"message": null
|
|
232
|
-
}
|
|
214
|
+
},
|
|
215
|
+
"liabilities": [
|
|
216
|
+
{
|
|
217
|
+
"account_id": "acc_loan_1",
|
|
218
|
+
"type": "student",
|
|
219
|
+
"balance": 12450.22,
|
|
220
|
+
"apr_percentage": 5.2,
|
|
221
|
+
"minimum_payment": 145,
|
|
222
|
+
"next_payment_due_date": "2026-03-22",
|
|
223
|
+
"is_overdue": false
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
"holdings": [
|
|
227
|
+
{
|
|
228
|
+
"account_id": "acc_inv_1",
|
|
229
|
+
"security_name": "Vanguard Total Stock Market ETF",
|
|
230
|
+
"symbol": "VTI",
|
|
231
|
+
"type": "etf",
|
|
232
|
+
"quantity": 12.5,
|
|
233
|
+
"value": 3541.25,
|
|
234
|
+
"cost_basis": 3010
|
|
235
|
+
}
|
|
236
|
+
],
|
|
237
|
+
"category_spending": [
|
|
238
|
+
{
|
|
239
|
+
"category": "FOOD_AND_DRINK",
|
|
240
|
+
"amount": 182.55
|
|
241
|
+
}
|
|
242
|
+
]
|
|
233
243
|
}
|
|
234
244
|
```
|
|
235
245
|
|
|
@@ -261,7 +271,6 @@ Returns:
|
|
|
261
271
|
## Development
|
|
262
272
|
|
|
263
273
|
```bash
|
|
264
|
-
cd mcp
|
|
265
274
|
npm install
|
|
266
275
|
npm run build
|
|
267
276
|
```
|
|
@@ -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.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
|
|
2
2
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
3
|
import { createWarmServer } from './server.js';
|
|
4
|
-
const DEFAULT_HTTP_HOST = '
|
|
4
|
+
const DEFAULT_HTTP_HOST = '127.0.0.1';
|
|
5
5
|
const DEFAULT_HTTP_PORT = 3000;
|
|
6
6
|
const DEFAULT_HTTP_PATH = '/mcp';
|
|
7
7
|
function parsePort(value, fallback) {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { startHttpServer } from './http.js';
|
|
3
2
|
import { install } from './install.js';
|
|
4
3
|
import { startStdioServer } from './server.js';
|
|
5
4
|
function printUsage() {
|
|
@@ -9,44 +8,11 @@ function printUsage() {
|
|
|
9
8
|
console.log('');
|
|
10
9
|
console.log(' warm-mcp [install] [--force] [--no-validate]');
|
|
11
10
|
console.log(' warm-mcp stdio');
|
|
12
|
-
console.log(' warm-mcp http [--host 0.0.0.0] [--port 3000] [--path /mcp]');
|
|
13
|
-
console.log(' [--allowed-hosts host1,host2]');
|
|
14
11
|
console.log('');
|
|
15
12
|
console.log(' Aliases:');
|
|
16
13
|
console.log(' --server, --stdio Start stdio mode');
|
|
17
|
-
console.log(' --http Start HTTP mode');
|
|
18
14
|
console.log('');
|
|
19
15
|
}
|
|
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
16
|
function parseCliArgs(args) {
|
|
51
17
|
const options = {
|
|
52
18
|
command: 'install',
|
|
@@ -67,10 +33,6 @@ function parseCliArgs(args) {
|
|
|
67
33
|
options.command = 'stdio';
|
|
68
34
|
continue;
|
|
69
35
|
}
|
|
70
|
-
if (arg === 'http' || arg === '--http') {
|
|
71
|
-
options.command = 'http';
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
36
|
if (arg === '--force') {
|
|
75
37
|
options.force = true;
|
|
76
38
|
continue;
|
|
@@ -79,30 +41,6 @@ function parseCliArgs(args) {
|
|
|
79
41
|
options.validateApiKey = false;
|
|
80
42
|
continue;
|
|
81
43
|
}
|
|
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
44
|
throw new Error(`Unknown argument: ${arg}`);
|
|
107
45
|
}
|
|
108
46
|
return options;
|
|
@@ -113,14 +51,6 @@ async function main() {
|
|
|
113
51
|
case 'help':
|
|
114
52
|
printUsage();
|
|
115
53
|
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
54
|
case 'stdio':
|
|
125
55
|
await startStdioServer();
|
|
126
56
|
return;
|
package/dist/install.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
|
3
3
|
import { dirname, join, resolve } from 'path';
|
|
4
4
|
import { homedir, platform } from 'os';
|
|
5
5
|
import { verifyWarmApiKey } from './server.js';
|
|
6
|
+
import { getWarmApiKeyPath } from './config-paths.js';
|
|
6
7
|
const HOME = homedir();
|
|
7
8
|
const CWD = process.cwd();
|
|
8
9
|
function isRecord(value) {
|
|
@@ -31,7 +32,11 @@ const GLOBAL_CLIENTS = [
|
|
|
31
32
|
configPath: join(HOME, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
32
33
|
format: 'json',
|
|
33
34
|
},
|
|
34
|
-
{
|
|
35
|
+
{
|
|
36
|
+
name: 'OpenCode',
|
|
37
|
+
configPath: join(HOME, '.config', 'opencode', 'opencode.json'),
|
|
38
|
+
format: 'json',
|
|
39
|
+
},
|
|
35
40
|
{ name: 'Codex CLI', configPath: join(HOME, '.codex', 'config.toml'), format: 'toml' },
|
|
36
41
|
{
|
|
37
42
|
name: 'Antigravity',
|
|
@@ -44,6 +49,7 @@ const PROJECT_CONFIGS = ['.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json'];
|
|
|
44
49
|
const MCP_CONFIG = platform() === 'win32'
|
|
45
50
|
? { command: 'cmd', args: ['/c', 'npx', '-y', '@warmio/mcp', '--server'] }
|
|
46
51
|
: { command: 'npx', args: ['-y', '@warmio/mcp', '--server'] };
|
|
52
|
+
const WARM_API_KEY_PATH = getWarmApiKeyPath();
|
|
47
53
|
function detectProjectClients() {
|
|
48
54
|
const found = [];
|
|
49
55
|
for (const name of PROJECT_CONFIGS) {
|
|
@@ -80,7 +86,7 @@ function isConfigured(client) {
|
|
|
80
86
|
return false;
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
|
-
function configureJson(client
|
|
89
|
+
function configureJson(client) {
|
|
84
90
|
let config = {};
|
|
85
91
|
if (existsSync(client.configPath)) {
|
|
86
92
|
try {
|
|
@@ -96,23 +102,25 @@ function configureJson(client, apiKey) {
|
|
|
96
102
|
const servers = config.mcpServers;
|
|
97
103
|
const existing = isRecord(servers.warm) ? servers.warm : undefined;
|
|
98
104
|
const existingEnv = isRecord(existing?.env) ? existing.env : {};
|
|
105
|
+
const nextEnv = { ...existingEnv };
|
|
106
|
+
delete nextEnv.WARM_API_KEY;
|
|
99
107
|
if (client.isProjectLevel && existing?.command) {
|
|
100
108
|
servers.warm = {
|
|
101
109
|
...existing,
|
|
102
|
-
env:
|
|
110
|
+
...(Object.keys(nextEnv).length > 0 ? { env: nextEnv } : {}),
|
|
103
111
|
};
|
|
104
112
|
}
|
|
105
113
|
else {
|
|
106
114
|
servers.warm = {
|
|
107
115
|
...existing,
|
|
108
116
|
...MCP_CONFIG,
|
|
109
|
-
env:
|
|
117
|
+
...(Object.keys(nextEnv).length > 0 ? { env: nextEnv } : {}),
|
|
110
118
|
};
|
|
111
119
|
}
|
|
112
120
|
mkdirSync(dirname(client.configPath), { recursive: true });
|
|
113
121
|
writeFileSync(client.configPath, JSON.stringify(config, null, 2) + '\n');
|
|
114
122
|
}
|
|
115
|
-
function configureToml(client
|
|
123
|
+
function configureToml(client) {
|
|
116
124
|
let content = '';
|
|
117
125
|
if (existsSync(client.configPath)) {
|
|
118
126
|
content = readFileSync(client.configPath, 'utf-8');
|
|
@@ -124,7 +132,7 @@ function configureToml(client, apiKey) {
|
|
|
124
132
|
const tomlArgs = platform() === 'win32'
|
|
125
133
|
? '["/c", "npx", "-y", "@warmio/mcp", "--server"]'
|
|
126
134
|
: '["-y", "@warmio/mcp", "--server"]';
|
|
127
|
-
const warmBlock = `[mcp_servers.warm]\ncommand = "${tomlCommand}"\nargs = ${tomlArgs}\n
|
|
135
|
+
const warmBlock = `[mcp_servers.warm]\ncommand = "${tomlCommand}"\nargs = ${tomlArgs}\n`;
|
|
128
136
|
const warmBlockPattern = /\n?\[mcp_servers\.warm\][\s\S]*?(?=\n\[[^\n]+\]|\s*$)/g;
|
|
129
137
|
let nextContent = content.replace(warmBlockPattern, '').trimEnd();
|
|
130
138
|
if (nextContent.length > 0) {
|
|
@@ -134,12 +142,12 @@ function configureToml(client, apiKey) {
|
|
|
134
142
|
mkdirSync(dirname(client.configPath), { recursive: true });
|
|
135
143
|
writeFileSync(client.configPath, nextContent.endsWith('\n') ? nextContent : `${nextContent}\n`);
|
|
136
144
|
}
|
|
137
|
-
function configure(client
|
|
145
|
+
function configure(client) {
|
|
138
146
|
if (client.format === 'json') {
|
|
139
|
-
configureJson(client
|
|
147
|
+
configureJson(client);
|
|
140
148
|
return;
|
|
141
149
|
}
|
|
142
|
-
configureToml(client
|
|
150
|
+
configureToml(client);
|
|
143
151
|
}
|
|
144
152
|
function shortPath(filePath) {
|
|
145
153
|
return filePath.replace(HOME, '~').replace(CWD, '.');
|
|
@@ -185,6 +193,10 @@ async function validateApiKey(apiKey) {
|
|
|
185
193
|
return true;
|
|
186
194
|
}
|
|
187
195
|
}
|
|
196
|
+
function storeApiKey(apiKey) {
|
|
197
|
+
mkdirSync(dirname(WARM_API_KEY_PATH), { recursive: true });
|
|
198
|
+
writeFileSync(WARM_API_KEY_PATH, `${apiKey}\n`, { mode: 0o600 });
|
|
199
|
+
}
|
|
188
200
|
export async function install(options = {}) {
|
|
189
201
|
const force = options.force ?? false;
|
|
190
202
|
const shouldValidateApiKey = options.validateApiKey ?? true;
|
|
@@ -222,11 +234,12 @@ export async function install(options = {}) {
|
|
|
222
234
|
return;
|
|
223
235
|
}
|
|
224
236
|
}
|
|
237
|
+
storeApiKey(apiKey);
|
|
225
238
|
console.log(' Configuring...');
|
|
226
239
|
console.log('');
|
|
227
240
|
needsSetup.forEach((client) => {
|
|
228
241
|
try {
|
|
229
|
-
configure(client
|
|
242
|
+
configure(client);
|
|
230
243
|
console.log(` ${client.name.padEnd(22)} done`);
|
|
231
244
|
}
|
|
232
245
|
catch (error) {
|
|
@@ -235,6 +248,7 @@ export async function install(options = {}) {
|
|
|
235
248
|
}
|
|
236
249
|
});
|
|
237
250
|
console.log('');
|
|
251
|
+
console.log(` Stored API key at ${shortPath(WARM_API_KEY_PATH)}`);
|
|
238
252
|
console.log(' All set! Restart your MCP clients and try:');
|
|
239
253
|
console.log(' "What\'s my net worth?"');
|
|
240
254
|
console.log('');
|
package/dist/schemas.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare const getTransactionsInputSchema: z.ZodObject<{
|
|
|
41
41
|
limit: z.ZodDefault<z.ZodInt>;
|
|
42
42
|
cursor: z.ZodOptional<z.ZodString>;
|
|
43
43
|
last_knowledge: z.ZodOptional<z.ZodString>;
|
|
44
|
+
search: z.ZodOptional<z.ZodString>;
|
|
44
45
|
}, z.core.$strict>;
|
|
45
46
|
export declare const transactionSchema: z.ZodObject<{
|
|
46
47
|
id: z.ZodNullable<z.ZodString>;
|
package/dist/schemas.js
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import * as z from 'zod/v4';
|
|
2
|
-
const dateSchema = z
|
|
3
|
-
.string()
|
|
4
|
-
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Expected date in YYYY-MM-DD format.');
|
|
2
|
+
const dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Expected date in YYYY-MM-DD format.');
|
|
5
3
|
const dateTimeSchema = z.string().datetime({ offset: true });
|
|
6
4
|
export const emptyInputSchema = z.object({}).strict().default({});
|
|
7
|
-
export const accountTypeSchema = z.enum([
|
|
8
|
-
'depository',
|
|
9
|
-
'credit',
|
|
10
|
-
'loan',
|
|
11
|
-
'investment',
|
|
12
|
-
'other',
|
|
13
|
-
]);
|
|
5
|
+
export const accountTypeSchema = z.enum(['depository', 'credit', 'loan', 'investment', 'other']);
|
|
14
6
|
export const accountSchema = z
|
|
15
7
|
.object({
|
|
16
8
|
name: z.string(),
|
|
@@ -42,6 +34,12 @@ const getTransactionsInputObjectSchema = z
|
|
|
42
34
|
last_knowledge: dateTimeSchema
|
|
43
35
|
.optional()
|
|
44
36
|
.describe('Incremental sync checkpoint from a prior get_transactions response. Cannot be combined with cursor.'),
|
|
37
|
+
search: z
|
|
38
|
+
.string()
|
|
39
|
+
.min(1)
|
|
40
|
+
.max(120)
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('Optional merchant or description search string. Keep it unchanged while following a cursor chain.'),
|
|
45
43
|
})
|
|
46
44
|
.strict()
|
|
47
45
|
.superRefine((value, ctx) => {
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -16,15 +16,17 @@ export type WarmToolRegistrationTarget = McpServer;
|
|
|
16
16
|
export type RegisteredWarmTools = Record<WarmToolName, RegisteredTool>;
|
|
17
17
|
export interface WarmApiClient {
|
|
18
18
|
getAccounts(): Promise<GetAccountsOutput>;
|
|
19
|
-
getTransactions(input: GetTransactionsInput): Promise<GetTransactionsOutput>;
|
|
20
19
|
getFinancialState(): Promise<GetFinancialStateOutput>;
|
|
20
|
+
getTransactions(input: GetTransactionsInput): Promise<GetTransactionsOutput>;
|
|
21
21
|
verifyKey(): Promise<VerifyKeyOutput>;
|
|
22
22
|
}
|
|
23
23
|
export interface WarmApiAccount {
|
|
24
|
+
account_id?: string | null;
|
|
24
25
|
name?: string | null;
|
|
25
26
|
type?: string | null;
|
|
26
27
|
subtype?: string | null;
|
|
27
28
|
current_balance?: number | null;
|
|
29
|
+
available_balance?: number | null;
|
|
28
30
|
institution_name?: string | null;
|
|
29
31
|
mask?: string | null;
|
|
30
32
|
}
|
|
@@ -34,6 +36,7 @@ export interface WarmApiAccountsResponse {
|
|
|
34
36
|
}
|
|
35
37
|
export interface WarmApiTransaction {
|
|
36
38
|
id?: string | null;
|
|
39
|
+
account_id?: string | null;
|
|
37
40
|
date?: string | null;
|
|
38
41
|
amount?: number | null;
|
|
39
42
|
merchant_name?: string | null;
|
package/dist/warm-server.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
-
import * as os from 'node:os';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
3
|
import { emptyInputSchema, getAccountsOutputSchema, getFinancialStateOutputSchema, getTransactionsInputSchema, getTransactionsOutputSchema, verifyKeyOutputSchema, } from './schemas.js';
|
|
4
|
+
import { getWarmApiKeyPath } from './config-paths.js';
|
|
6
5
|
const DEFAULT_API_URL = process.env.WARM_API_URL || 'https://warm.io';
|
|
7
6
|
const DEFAULT_REQUEST_TIMEOUT_MS = (() => {
|
|
8
7
|
const raw = Number(process.env.WARM_API_TIMEOUT_MS || 10_000);
|
|
@@ -63,70 +62,6 @@ function normalizeAccount(account) {
|
|
|
63
62
|
mask: account.mask ?? null,
|
|
64
63
|
};
|
|
65
64
|
}
|
|
66
|
-
function normalizeSnapshot(snapshot) {
|
|
67
|
-
const date = snapshot.snapshot_date || snapshot.period_end || null;
|
|
68
|
-
if (!date) {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
const totalAssets = snapshot.total_assets ??
|
|
72
|
-
(snapshot.total_cash ?? 0) + (snapshot.investment_value ?? 0) + (snapshot.asset_value ?? 0);
|
|
73
|
-
const totalLiabilities = snapshot.total_liabilities ?? snapshot.total_debt ?? snapshot.liability_value ?? 0;
|
|
74
|
-
return {
|
|
75
|
-
date,
|
|
76
|
-
net_worth: roundMoney(snapshot.net_worth ?? totalAssets - totalLiabilities),
|
|
77
|
-
total_assets: roundMoney(totalAssets),
|
|
78
|
-
total_liabilities: roundMoney(totalLiabilities),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
function normalizeRecurring(stream) {
|
|
82
|
-
return {
|
|
83
|
-
merchant: stream.merchant_name || stream.description || 'Unknown',
|
|
84
|
-
amount: roundMoney(Math.abs(stream.average_amount ?? stream.last_amount ?? 0)),
|
|
85
|
-
frequency: stream.frequency || 'UNKNOWN',
|
|
86
|
-
next_date: stream.next_date ?? null,
|
|
87
|
-
type: stream.stream_type ?? null,
|
|
88
|
-
active: stream.is_active !== false,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
function normalizeGoal(goal) {
|
|
92
|
-
return {
|
|
93
|
-
name: goal.name || 'Unnamed Goal',
|
|
94
|
-
target: roundMoney(goal.target ?? 0),
|
|
95
|
-
current: roundMoney(goal.current ?? 0),
|
|
96
|
-
progress_percent: roundMoney(goal.progress_percent ?? 0),
|
|
97
|
-
target_date: goal.target_date ?? null,
|
|
98
|
-
status: goal.status ?? null,
|
|
99
|
-
category: goal.category ?? null,
|
|
100
|
-
monthly_contribution_needed: goal.monthly_contribution_needed == null
|
|
101
|
-
? null
|
|
102
|
-
: roundMoney(goal.monthly_contribution_needed),
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
function normalizeLiability(liability) {
|
|
106
|
-
return {
|
|
107
|
-
account_id: liability.account_id || '',
|
|
108
|
-
type: liability.type || 'unknown',
|
|
109
|
-
balance: liability.balance == null ? null : roundMoney(liability.balance),
|
|
110
|
-
apr_percentage: liability.apr_percentage ?? liability.interest_rate_percentage ?? null,
|
|
111
|
-
minimum_payment: liability.minimum_payment == null ? null : roundMoney(liability.minimum_payment),
|
|
112
|
-
next_payment_due_date: liability.next_payment_due_date ?? null,
|
|
113
|
-
is_overdue: liability.is_overdue ?? null,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
function normalizeHolding(holding) {
|
|
117
|
-
return {
|
|
118
|
-
account_id: holding.account_id || '',
|
|
119
|
-
security_name: holding.security_name ?? null,
|
|
120
|
-
symbol: holding.symbol ?? null,
|
|
121
|
-
type: holding.type ?? null,
|
|
122
|
-
quantity: holding.quantity ?? 0,
|
|
123
|
-
value: holding.value == null ? null : roundMoney(holding.value),
|
|
124
|
-
cost_basis: holding.cost_basis == null ? null : roundMoney(holding.cost_basis),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function asGeneratedAt(...values) {
|
|
128
|
-
return values.find((value) => Boolean(value)) || new Date().toISOString();
|
|
129
|
-
}
|
|
130
65
|
function createWarmApiClientConfig(options) {
|
|
131
66
|
return {
|
|
132
67
|
apiUrl: options.apiUrl || DEFAULT_API_URL,
|
|
@@ -143,7 +78,7 @@ export function getConfiguredApiKey() {
|
|
|
143
78
|
cachedApiKey = process.env.WARM_API_KEY.trim();
|
|
144
79
|
return cachedApiKey;
|
|
145
80
|
}
|
|
146
|
-
const configPath =
|
|
81
|
+
const configPath = getWarmApiKeyPath();
|
|
147
82
|
try {
|
|
148
83
|
cachedApiKey = fs.readFileSync(configPath, 'utf-8').trim() || null;
|
|
149
84
|
}
|
|
@@ -229,6 +164,7 @@ export function createWarmApiClient(options = {}) {
|
|
|
229
164
|
limit: String(input.limit),
|
|
230
165
|
cursor: input.cursor,
|
|
231
166
|
last_knowledge: input.last_knowledge,
|
|
167
|
+
search: input.search,
|
|
232
168
|
}, options);
|
|
233
169
|
const nextCursor = response.pagination?.next_cursor ?? null;
|
|
234
170
|
return {
|
|
@@ -251,56 +187,7 @@ export function createWarmApiClient(options = {}) {
|
|
|
251
187
|
};
|
|
252
188
|
}
|
|
253
189
|
async function getFinancialState() {
|
|
254
|
-
|
|
255
|
-
apiRequest('/api/export', { dataset: 'snapshots' }, options),
|
|
256
|
-
apiRequest('/api/export', { dataset: 'recurring' }, options),
|
|
257
|
-
apiRequest('/api/export', { dataset: 'budgets' }, options),
|
|
258
|
-
apiRequest('/api/export', { dataset: 'goals' }, options),
|
|
259
|
-
apiRequest('/api/export', { dataset: 'health' }, options),
|
|
260
|
-
apiRequest('/api/export', { dataset: 'liabilities' }, options),
|
|
261
|
-
apiRequest('/api/export', { dataset: 'holdings' }, options),
|
|
262
|
-
]);
|
|
263
|
-
// Extract category spending from the most recent snapshot's spending_by_category
|
|
264
|
-
const snapshots = snapshotsResponse.snapshots || [];
|
|
265
|
-
const latestWithCategories = snapshots.find((s) => Array.isArray(s.spending_by_category));
|
|
266
|
-
const categorySpending = (latestWithCategories?.spending_by_category || []).map((c) => ({
|
|
267
|
-
category: c.category || 'Unknown',
|
|
268
|
-
amount: roundMoney(Math.abs(c.amount ?? 0)),
|
|
269
|
-
}));
|
|
270
|
-
return {
|
|
271
|
-
generated_at: asGeneratedAt(snapshotsResponse.generated_at, recurringResponse.generated_at, budgetsResponse.generated_at, goalsResponse.generated_at, healthResponse.generated_at),
|
|
272
|
-
snapshots: snapshots
|
|
273
|
-
.map(normalizeSnapshot)
|
|
274
|
-
.filter((snapshot) => snapshot !== null),
|
|
275
|
-
recurring: (recurringResponse.recurring_transactions || []).map(normalizeRecurring),
|
|
276
|
-
budgets: (budgetsResponse.budgets || []).map((budget) => ({
|
|
277
|
-
name: budget.name || 'Unnamed Budget',
|
|
278
|
-
amount: roundMoney(budget.amount ?? 0),
|
|
279
|
-
spent: roundMoney(budget.spent ?? 0),
|
|
280
|
-
remaining: roundMoney(budget.remaining ?? 0),
|
|
281
|
-
percent_used: roundMoney(budget.percent_used ?? 0),
|
|
282
|
-
period: budget.period || 'monthly',
|
|
283
|
-
status: budget.status ?? null,
|
|
284
|
-
})),
|
|
285
|
-
goals: (goalsResponse.goals || []).map(normalizeGoal),
|
|
286
|
-
health: {
|
|
287
|
-
score: healthResponse.score ?? null,
|
|
288
|
-
label: healthResponse.label ?? null,
|
|
289
|
-
data_completeness: healthResponse.data_completeness ?? null,
|
|
290
|
-
pillars: healthResponse.pillars
|
|
291
|
-
? {
|
|
292
|
-
spend: healthResponse.pillars.spend ?? null,
|
|
293
|
-
save: healthResponse.pillars.save ?? null,
|
|
294
|
-
borrow: healthResponse.pillars.borrow ?? null,
|
|
295
|
-
build: healthResponse.pillars.build ?? null,
|
|
296
|
-
}
|
|
297
|
-
: null,
|
|
298
|
-
message: healthResponse.message ?? null,
|
|
299
|
-
},
|
|
300
|
-
liabilities: (liabilitiesResponse.liabilities || []).map(normalizeLiability),
|
|
301
|
-
holdings: (holdingsResponse.holdings || []).map(normalizeHolding),
|
|
302
|
-
category_spending: categorySpending,
|
|
303
|
-
};
|
|
190
|
+
return apiRequest('/api/financial-state', {}, options);
|
|
304
191
|
}
|
|
305
192
|
async function verifyKey() {
|
|
306
193
|
const response = await apiRequest('/api/verify', {}, options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warmio/mcp",
|
|
3
|
-
"version": "4.1
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"description": "MCP server for Warm Financial API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,10 +14,6 @@
|
|
|
14
14
|
"types": "./dist/server.d.ts",
|
|
15
15
|
"default": "./dist/server.js"
|
|
16
16
|
},
|
|
17
|
-
"./http": {
|
|
18
|
-
"types": "./dist/http.d.ts",
|
|
19
|
-
"default": "./dist/http.js"
|
|
20
|
-
},
|
|
21
17
|
"./install": {
|
|
22
18
|
"types": "./dist/install.d.ts",
|
|
23
19
|
"default": "./dist/install.js"
|
|
@@ -32,7 +28,6 @@
|
|
|
32
28
|
"scripts": {
|
|
33
29
|
"build": "tsc",
|
|
34
30
|
"start": "node dist/index.js --stdio",
|
|
35
|
-
"start:http": "node dist/index.js --http",
|
|
36
31
|
"start:install": "node dist/index.js install",
|
|
37
32
|
"prepublishOnly": "npm run build"
|
|
38
33
|
},
|