@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Warmio
2
2
 
3
- Warmio is the one-command CLI and read-only MCP server for Warm financial data.
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 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.
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 Warm API key from [Settings -> API Keys](https://warm.io/settings)
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
- Use this when a client asks you to paste an MCP JSON config:
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
- Optional auth overrides:
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
- - `WARM_API_KEY`
52
- - `WARM_API_KEY_FILE`
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
- ## Core Tools
151
+ ## MCP Tools
109
152
 
110
- Warm's v6 MCP surface is exactly three tools:
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
- ## v6 Breaking Changes
296
+ ## v7 Breaking Changes
243
297
 
244
- v6 is a clean break with no backwards compatibility and no alias window.
298
+ v7 is a clean pre-launch cutover with no backwards compatibility or alias window.
245
299
 
246
- - `get_accounts` is removed. Use `get_financial_context.status.accounts`.
247
- - `get_financial_state` is removed. Use `get_financial_context`.
248
- - `get_transactions` no longer supports `cursor`, `search`, or `last_knowledge`.
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
- - Read-only: no write, delete, transfer, or mutation tools
256
- - Scoped: the key only reads the owner's Warm data
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
@@ -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;
@@ -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
- if (process.env.WARM_API_KEY_FILE?.trim()) {
21
- return process.env.WARM_API_KEY_FILE.trim();
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(), 'api_key');
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
- const server = createWarmServer();
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
- export declare function startStdioServer(): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
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
@@ -1,5 +1,7 @@
1
+ import { type WarmApiAudience } from './config-paths.js';
1
2
  export interface InstallOptions {
2
3
  force?: boolean;
4
+ mode?: WarmApiAudience;
3
5
  validateApiKey?: boolean;
4
6
  }
5
7
  export declare function install(options?: InstallOptions): Promise<void>;