@warmio/mcp 6.0.0 → 7.0.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 +78 -25
- package/dist/config-paths.d.ts +2 -1
- package/dist/config-paths.js +6 -4
- package/dist/http.d.ts +6 -0
- package/dist/http.js +36 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +18 -6
- package/dist/install.d.ts +2 -0
- package/dist/install.js +209 -94
- package/dist/schemas.d.ts +46 -6
- package/dist/schemas.js +24 -0
- package/dist/server.d.ts +5 -3
- package/dist/server.js +76 -12
- package/dist/types.d.ts +5 -2
- package/dist/types.js +7 -1
- package/dist/warm-api-client.d.ts +54 -3
- package/dist/warm-api-client.js +99 -41
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Warmio
|
|
2
2
|
|
|
3
|
-
Warmio is the one-command CLI
|
|
3
|
+
Warmio is the one-command CLI with separate compact MCP modes for financial context and supported automations.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,26 +8,32 @@ Warmio is the one-command CLI and read-only MCP server for Warm financial data.
|
|
|
8
8
|
npx -y @warmio/mcp@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The installer detects supported MCP clients, prompts for
|
|
12
|
-
writes the local `stdio` MCP config automatically.
|
|
13
|
-
|
|
11
|
+
The installer detects supported MCP clients, prompts for a mode-specific Warm API key, validates
|
|
12
|
+
its audience, and writes the local `stdio` MCP config automatically. Context and automation keys
|
|
13
|
+
are stored separately and are never shared between modes.
|
|
14
14
|
|
|
15
15
|
## Requirements
|
|
16
16
|
|
|
17
17
|
- Warm Pro
|
|
18
|
-
- A
|
|
18
|
+
- A context key or an automation key from [Settings -> API Keys](https://warm.io/settings)
|
|
19
19
|
- Node.js 18+
|
|
20
20
|
|
|
21
21
|
## Manual MCP Config
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Context is the default read-only mode. Install it with its own key:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx -y @warmio/mcp@latest install --mode context
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use this when a client asks you to paste a context MCP JSON config:
|
|
24
30
|
|
|
25
31
|
```json
|
|
26
32
|
{
|
|
27
33
|
"mcpServers": {
|
|
28
34
|
"warm": {
|
|
29
35
|
"command": "npx",
|
|
30
|
-
"args": ["-y", "@warmio/mcp@latest", "mcp"]
|
|
36
|
+
"args": ["-y", "@warmio/mcp@latest", "mcp", "--mode", "context"]
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
}
|
|
@@ -40,16 +46,46 @@ On Windows, prefer:
|
|
|
40
46
|
"mcpServers": {
|
|
41
47
|
"warm": {
|
|
42
48
|
"command": "cmd",
|
|
43
|
-
"args": ["/c", "npx", "-y", "@warmio/mcp@latest", "mcp"]
|
|
49
|
+
"args": ["/c", "npx", "-y", "@warmio/mcp@latest", "mcp", "--mode", "context"]
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
```
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
Context credential overrides:
|
|
56
|
+
|
|
57
|
+
- `WARM_CONTEXT_API_KEY`
|
|
58
|
+
- `WARM_CONTEXT_API_KEY_FILE`
|
|
59
|
+
|
|
60
|
+
Automation is intentionally opt-in and must use a distinct automation key:
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
-
|
|
62
|
+
```bash
|
|
63
|
+
npx -y @warmio/mcp@latest install --mode automation
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"warm-automation": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "@warmio/mcp@latest", "mcp", "--mode", "automation"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Automation credential overrides are `WARM_AUTOMATION_API_KEY` and
|
|
78
|
+
`WARM_AUTOMATION_API_KEY_FILE`. The default key files are `${WARM_CONFIG_DIR}/context_api_key`
|
|
79
|
+
and `${WARM_CONFIG_DIR}/automation_api_key`; `WARM_API_KEY` and `WARM_API_KEY_FILE` are not read.
|
|
80
|
+
The API verifies the key audience before every MCP tool call, so a context key cannot operate the
|
|
81
|
+
automation server and an automation key cannot operate the context server.
|
|
82
|
+
|
|
83
|
+
Installer validation fails closed for API responses `401` and `403`. A `429` rate limit or a
|
|
84
|
+
temporary network failure remains a soft validation failure so setup can finish; the MCP server
|
|
85
|
+
will still validate the key audience before serving tools.
|
|
86
|
+
|
|
87
|
+
Both modes send API requests to `https://app.warm.io` by default. Set `WARM_API_URL` only when an
|
|
88
|
+
explicit API base URL override is required.
|
|
53
89
|
|
|
54
90
|
## Universal Copy/Paste Setup Prompt
|
|
55
91
|
|
|
@@ -62,7 +98,7 @@ Use this MCP server config:
|
|
|
62
98
|
"mcpServers": {
|
|
63
99
|
"warm": {
|
|
64
100
|
"command": "npx",
|
|
65
|
-
"args": ["-y", "@warmio/mcp@latest", "mcp"]
|
|
101
|
+
"args": ["-y", "@warmio/mcp@latest", "mcp", "--mode", "context"]
|
|
66
102
|
}
|
|
67
103
|
}
|
|
68
104
|
}
|
|
@@ -95,7 +131,7 @@ All CLI read commands print JSON.
|
|
|
95
131
|
If your MCP client supports Streamable HTTP, you can run:
|
|
96
132
|
|
|
97
133
|
```bash
|
|
98
|
-
npx -y @warmio/mcp@latest http --host 127.0.0.1 --port 3000 --path /mcp
|
|
134
|
+
npx -y @warmio/mcp@latest http --mode context --host 127.0.0.1 --port 3000 --path /mcp
|
|
99
135
|
```
|
|
100
136
|
|
|
101
137
|
Relevant optional environment variables:
|
|
@@ -104,10 +140,17 @@ Relevant optional environment variables:
|
|
|
104
140
|
- `WARM_MCP_HTTP_PORT`
|
|
105
141
|
- `WARM_MCP_HTTP_PATH`
|
|
106
142
|
- `WARM_MCP_ALLOWED_HOSTS`
|
|
143
|
+
- `WARM_MCP_AUTH_TOKEN`
|
|
144
|
+
|
|
145
|
+
`WARM_MCP_AUTH_TOKEN` is a separate bearer token for the MCP HTTP transport, never a Warm API
|
|
146
|
+
key. Automation HTTP always requires it, including on loopback. Context HTTP requires it whenever
|
|
147
|
+
the server binds anything other than the exact SDK-protected loopback hosts `127.0.0.1`,
|
|
148
|
+
`localhost`, or `::1`. Send it on every request as
|
|
149
|
+
`Authorization: Bearer <WARM_MCP_AUTH_TOKEN>`.
|
|
107
150
|
|
|
108
|
-
##
|
|
151
|
+
## MCP Tools
|
|
109
152
|
|
|
110
|
-
|
|
153
|
+
Context mode remains exactly three read-only tools:
|
|
111
154
|
|
|
112
155
|
| Tool | Description |
|
|
113
156
|
| ----------------------- | ------------------------------------------------ |
|
|
@@ -115,6 +158,14 @@ Warm's v6 MCP surface is exactly three tools:
|
|
|
115
158
|
| `get_transactions` | Return one month page or the fixed latest window |
|
|
116
159
|
| `verify_key` | Validate the configured API key |
|
|
117
160
|
|
|
161
|
+
Automation mode is also exactly three tools:
|
|
162
|
+
|
|
163
|
+
| Tool | Description |
|
|
164
|
+
| -------------------- | ------------------------------------------------------------------ |
|
|
165
|
+
| `search_operations` | Find supported operations without loading the full catalog |
|
|
166
|
+
| `describe_operation` | Read an operation schema and request approval for a write input |
|
|
167
|
+
| `invoke_operation` | Invoke the operation; every write requires an `approval_id` |
|
|
168
|
+
|
|
118
169
|
## Parameter Contract
|
|
119
170
|
|
|
120
171
|
- Every tool takes a JSON object input and returns a JSON object output.
|
|
@@ -128,6 +179,9 @@ Warm's v6 MCP surface is exactly three tools:
|
|
|
128
179
|
- A well-formed month outside the covered range returns an error.
|
|
129
180
|
- A well-formed month inside the covered range with no transactions returns `count: 0` and `items: []`.
|
|
130
181
|
- Transaction amounts follow the Plaid sign convention: positive = expense/debit, negative = income/credit.
|
|
182
|
+
- `describe_operation` returns `approval: { id, status, approval_url, expires_at }` for every write operation.
|
|
183
|
+
- `invoke_operation` accepts that approval as `approval_id`; confirmation tokens are not supported.
|
|
184
|
+
- Tool and Warm API failures return structured MCP results with `isError: true`; malformed MCP input remains a protocol error.
|
|
131
185
|
|
|
132
186
|
## Shapes
|
|
133
187
|
|
|
@@ -239,21 +293,20 @@ Returns:
|
|
|
239
293
|
}
|
|
240
294
|
```
|
|
241
295
|
|
|
242
|
-
##
|
|
296
|
+
## v7 Breaking Changes
|
|
243
297
|
|
|
244
|
-
|
|
298
|
+
v7 is a clean pre-launch cutover with no backwards compatibility or alias window.
|
|
245
299
|
|
|
246
|
-
-
|
|
247
|
-
- `
|
|
248
|
-
-
|
|
249
|
-
- The old `txns` transaction array is gone. Transaction responses use `items`.
|
|
250
|
-
- Renamed fields follow the FinancialContext contract, including `subcategory`, `used_percent`,
|
|
251
|
-
`status`, `due_date`, and `other_assets`.
|
|
300
|
+
- API keys are audience-bound: context keys cannot invoke actions and automation keys cannot run context mode.
|
|
301
|
+
- `X-API-Key` is removed. Use `Authorization: Bearer`.
|
|
302
|
+
- Automation is a separate opt-in MCP server mode, not extra tools on the context server.
|
|
252
303
|
|
|
253
304
|
## Security
|
|
254
305
|
|
|
255
|
-
-
|
|
256
|
-
-
|
|
306
|
+
- Context mode is read-only and uses a context-only key.
|
|
307
|
+
- Automation mode uses a separate automation-only key and a curated server-side operation allowlist.
|
|
308
|
+
- Streamable HTTP is bearer-protected with an independent `WARM_MCP_AUTH_TOKEN` where required.
|
|
309
|
+
- Write operations return an approval `{ id, status, approval_url, expires_at }`; invoke the exact input with its `approval_id`.
|
|
257
310
|
- Revocable: delete the key in Settings to revoke access immediately
|
|
258
311
|
|
|
259
312
|
## Development
|
package/dist/config-paths.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type WarmApiAudience = 'automation' | 'context';
|
|
1
2
|
export declare function getWarmConfigDir(): string;
|
|
2
|
-
export declare function getWarmApiKeyPath(): string;
|
|
3
|
+
export declare function getWarmApiKeyPath(audience?: WarmApiAudience): string;
|
|
3
4
|
export declare function readConfigFile(configPath: string): string | null;
|
package/dist/config-paths.js
CHANGED
|
@@ -16,11 +16,13 @@ export function getWarmConfigDir() {
|
|
|
16
16
|
}
|
|
17
17
|
return path.join(os.homedir(), '.config', 'warm');
|
|
18
18
|
}
|
|
19
|
-
export function getWarmApiKeyPath() {
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
export function getWarmApiKeyPath(audience = 'context') {
|
|
20
|
+
const envKey = audience === 'automation' ? 'WARM_AUTOMATION_API_KEY_FILE' : 'WARM_CONTEXT_API_KEY_FILE';
|
|
21
|
+
const configuredPath = process.env[envKey]?.trim();
|
|
22
|
+
if (configuredPath) {
|
|
23
|
+
return configuredPath;
|
|
22
24
|
}
|
|
23
|
-
return path.join(getWarmConfigDir(),
|
|
25
|
+
return path.join(getWarmConfigDir(), `${audience}_api_key`);
|
|
24
26
|
}
|
|
25
27
|
export function readConfigFile(configPath) {
|
|
26
28
|
try {
|
package/dist/http.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { type WarmServerMode } from './server.js';
|
|
1
2
|
export interface HttpServerOptions {
|
|
2
3
|
allowedHosts?: string[];
|
|
4
|
+
authToken?: string;
|
|
3
5
|
host?: string;
|
|
6
|
+
mode?: WarmServerMode;
|
|
4
7
|
path?: string;
|
|
5
8
|
port?: number;
|
|
6
9
|
}
|
|
10
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
11
|
+
export declare function validateHttpSecurity(options: Pick<HttpServerOptions, 'authToken' | 'host' | 'mode'>): void;
|
|
12
|
+
export declare function hasValidTransportBearerToken(authorization: string | undefined, authToken: string): boolean;
|
|
7
13
|
export declare function resolveHttpServerOptions(overrides?: HttpServerOptions): Required<HttpServerOptions>;
|
|
8
14
|
export declare function startHttpServer(options?: HttpServerOptions): Promise<any>;
|
package/dist/http.js
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
|
|
2
2
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
3
4
|
import { createWarmServer } from './server.js';
|
|
4
5
|
const DEFAULT_HTTP_HOST = '127.0.0.1';
|
|
5
6
|
const DEFAULT_HTTP_PORT = 3000;
|
|
6
7
|
const DEFAULT_HTTP_PATH = '/mcp';
|
|
8
|
+
export function isLoopbackHost(host) {
|
|
9
|
+
const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
10
|
+
return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1';
|
|
11
|
+
}
|
|
12
|
+
export function validateHttpSecurity(options) {
|
|
13
|
+
const mode = options.mode || 'context';
|
|
14
|
+
const hasAuthToken = Boolean(options.authToken?.trim());
|
|
15
|
+
if (mode === 'automation' && !hasAuthToken) {
|
|
16
|
+
throw new Error('Automation MCP HTTP requires WARM_MCP_AUTH_TOKEN.');
|
|
17
|
+
}
|
|
18
|
+
if (!isLoopbackHost(options.host || DEFAULT_HTTP_HOST) && !hasAuthToken) {
|
|
19
|
+
throw new Error('Non-loopback MCP HTTP requires WARM_MCP_AUTH_TOKEN.');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function hasValidTransportBearerToken(authorization, authToken) {
|
|
23
|
+
const expected = `Bearer ${authToken}`;
|
|
24
|
+
if (!authorization) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const actualBuffer = Buffer.from(authorization);
|
|
28
|
+
const expectedBuffer = Buffer.from(expected);
|
|
29
|
+
if (actualBuffer.length !== expectedBuffer.length) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return timingSafeEqual(actualBuffer, expectedBuffer);
|
|
33
|
+
}
|
|
7
34
|
function parsePort(value, fallback) {
|
|
8
35
|
const parsed = Number(value);
|
|
9
36
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
@@ -37,11 +64,13 @@ export function resolveHttpServerOptions(overrides = {}) {
|
|
|
37
64
|
return {
|
|
38
65
|
allowedHosts: overrides.allowedHosts ??
|
|
39
66
|
parseAllowedHosts(process.env.WARM_MCP_ALLOWED_HOSTS || process.env.MCP_ALLOWED_HOSTS),
|
|
67
|
+
authToken: overrides.authToken ?? process.env.WARM_MCP_AUTH_TOKEN?.trim() ?? '',
|
|
40
68
|
host: overrides.host ||
|
|
41
69
|
process.env.WARM_MCP_HTTP_HOST ||
|
|
42
70
|
process.env.MCP_HOST ||
|
|
43
71
|
process.env.HOST ||
|
|
44
72
|
DEFAULT_HTTP_HOST,
|
|
73
|
+
mode: overrides.mode ?? 'context',
|
|
45
74
|
path: normalizePath(overrides.path || process.env.WARM_MCP_HTTP_PATH || process.env.MCP_PATH || DEFAULT_HTTP_PATH),
|
|
46
75
|
port: overrides.port ??
|
|
47
76
|
parsePort(process.env.WARM_MCP_HTTP_PORT || process.env.MCP_PORT || process.env.PORT, DEFAULT_HTTP_PORT),
|
|
@@ -54,12 +83,18 @@ function sendJsonMethodNotAllowed(res) {
|
|
|
54
83
|
}
|
|
55
84
|
export async function startHttpServer(options = {}) {
|
|
56
85
|
const resolved = resolveHttpServerOptions(options);
|
|
86
|
+
validateHttpSecurity(resolved);
|
|
57
87
|
const app = createMcpExpressApp({
|
|
58
88
|
...(resolved.allowedHosts.length > 0 ? { allowedHosts: resolved.allowedHosts } : {}),
|
|
59
89
|
host: resolved.host,
|
|
60
90
|
});
|
|
61
91
|
app.post(resolved.path, async (req, res) => {
|
|
62
|
-
|
|
92
|
+
if (resolved.authToken &&
|
|
93
|
+
!hasValidTransportBearerToken(req.get('authorization'), resolved.authToken)) {
|
|
94
|
+
res.status(401).json(jsonRpcError('Unauthorized MCP transport request.'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const server = createWarmServer({ mode: resolved.mode });
|
|
63
98
|
const transport = new StreamableHTTPServerTransport({
|
|
64
99
|
sessionIdGenerator: undefined,
|
|
65
100
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import { type WarmServerMode } from './server.js';
|
|
3
|
+
export declare function startStdioServer(mode?: WarmServerMode): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
|
package/dist/index.js
CHANGED
|
@@ -16,9 +16,9 @@ function printUsage() {
|
|
|
16
16
|
console.log(' warmio');
|
|
17
17
|
console.log(' Run the interactive installer');
|
|
18
18
|
console.log('');
|
|
19
|
-
console.log(' warmio install [--force] [--no-validate]');
|
|
20
|
-
console.log(' warmio mcp');
|
|
21
|
-
console.log(' warmio http [--host 127.0.0.1] [--port 3000] [--path /mcp]');
|
|
19
|
+
console.log(' warmio install --mode context|automation [--force] [--no-validate]');
|
|
20
|
+
console.log(' warmio mcp [--mode context|automation]');
|
|
21
|
+
console.log(' warmio http [--mode context|automation] [--host 127.0.0.1] [--port 3000] [--path /mcp]');
|
|
22
22
|
console.log(' warmio context get');
|
|
23
23
|
console.log(' warmio context meta');
|
|
24
24
|
console.log(' warmio transactions get --month YYYY-MM');
|
|
@@ -32,6 +32,7 @@ function parseCliArgs(args) {
|
|
|
32
32
|
const options = {
|
|
33
33
|
command: isInteractiveSession() ? 'install' : 'mcp',
|
|
34
34
|
force: false,
|
|
35
|
+
mode: 'context',
|
|
35
36
|
validateApiKey: true,
|
|
36
37
|
};
|
|
37
38
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -99,6 +100,15 @@ function parseCliArgs(args) {
|
|
|
99
100
|
options.latest = true;
|
|
100
101
|
continue;
|
|
101
102
|
}
|
|
103
|
+
if (arg === '--mode') {
|
|
104
|
+
const mode = args[index + 1];
|
|
105
|
+
if (mode !== 'context' && mode !== 'automation') {
|
|
106
|
+
throw new Error('Expected --mode context or --mode automation.');
|
|
107
|
+
}
|
|
108
|
+
options.mode = mode;
|
|
109
|
+
index += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
102
112
|
if (arg === '--host') {
|
|
103
113
|
options.httpHost = args[index + 1];
|
|
104
114
|
index += 1;
|
|
@@ -151,8 +161,8 @@ function validateCliOptions(options) {
|
|
|
151
161
|
function printJson(value) {
|
|
152
162
|
console.log(JSON.stringify(value, null, 2));
|
|
153
163
|
}
|
|
154
|
-
export async function startStdioServer() {
|
|
155
|
-
const server = createWarmServer();
|
|
164
|
+
export async function startStdioServer(mode = 'context') {
|
|
165
|
+
const server = createWarmServer({ mode });
|
|
156
166
|
const transport = new AdaptiveStdioTransport();
|
|
157
167
|
await server.connect(transport);
|
|
158
168
|
return server;
|
|
@@ -175,18 +185,20 @@ async function main() {
|
|
|
175
185
|
printUsage();
|
|
176
186
|
return;
|
|
177
187
|
case 'mcp':
|
|
178
|
-
await startStdioServer();
|
|
188
|
+
await startStdioServer(options.mode);
|
|
179
189
|
return;
|
|
180
190
|
case 'http':
|
|
181
191
|
await startHttpServer({
|
|
182
192
|
host: options.httpHost,
|
|
183
193
|
path: options.httpPath,
|
|
184
194
|
port: options.httpPort,
|
|
195
|
+
mode: options.mode,
|
|
185
196
|
});
|
|
186
197
|
return;
|
|
187
198
|
case 'install':
|
|
188
199
|
await install({
|
|
189
200
|
force: options.force,
|
|
201
|
+
mode: options.mode,
|
|
190
202
|
validateApiKey: options.validateApiKey,
|
|
191
203
|
});
|
|
192
204
|
return;
|
package/dist/install.d.ts
CHANGED