@warmio/mcp 7.0.1 → 9.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
@@ -14,7 +14,7 @@ are stored separately and are never shared between modes.
14
14
 
15
15
  ## Requirements
16
16
 
17
- - Warm Pro
17
+ - Any paid Warm plan (Plus or Pro)
18
18
  - A context key or an automation key from [Settings -> API Keys](https://warm.io/settings)
19
19
  - Node.js 18+
20
20
 
@@ -272,7 +272,20 @@ Latest response:
272
272
  "since": "2026-06-26",
273
273
  "window_days": 10,
274
274
  "count": 1,
275
- "items": []
275
+ "items": [
276
+ {
277
+ "id": "txn_1",
278
+ "account_id": "acct_1",
279
+ "date": "2026-07-05",
280
+ "amount": 42.18,
281
+ "merchant": "Whole Foods",
282
+ "name": "WHOLE FOODS",
283
+ "category": "FOOD_AND_DRINK",
284
+ "subcategory": "FOOD_AND_DRINK_GROCERIES",
285
+ "pending": false,
286
+ "currency": "USD"
287
+ }
288
+ ]
276
289
  }
277
290
  ```
278
291
 
@@ -293,14 +306,6 @@ Returns:
293
306
  }
294
307
  ```
295
308
 
296
- ## v7 Breaking Changes
297
-
298
- v7 is a clean pre-launch cutover with no backwards compatibility or alias window.
299
-
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.
303
-
304
309
  ## Security
305
310
 
306
311
  - Context mode is read-only and uses a context-only key.
@@ -1,4 +1,4 @@
1
- export type WarmApiAudience = 'automation' | 'context';
1
+ import { type PrivateMcpMode } from '@warmio/contracts/mcp';
2
2
  export declare function getWarmConfigDir(): string;
3
- export declare function getWarmApiKeyPath(audience?: WarmApiAudience): string;
3
+ export declare function getWarmApiKeyPath(audience?: PrivateMcpMode): string;
4
4
  export declare function readConfigFile(configPath: string): string | null;
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as os from 'node:os';
3
3
  import * as path from 'node:path';
4
+ import { WARM_MCP_CREDENTIALS } from '@warmio/contracts/mcp';
4
5
  export function getWarmConfigDir() {
5
6
  if (process.env.WARM_CONFIG_DIR?.trim()) {
6
7
  return process.env.WARM_CONFIG_DIR.trim();
@@ -17,12 +18,12 @@ export function getWarmConfigDir() {
17
18
  return path.join(os.homedir(), '.config', 'warm');
18
19
  }
19
20
  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();
21
+ const credential = WARM_MCP_CREDENTIALS[audience];
22
+ const configuredPath = process.env[credential.apiKeyFileEnv]?.trim();
22
23
  if (configuredPath) {
23
24
  return configuredPath;
24
25
  }
25
- return path.join(getWarmConfigDir(), `${audience}_api_key`);
26
+ return path.join(getWarmConfigDir(), credential.defaultFileName);
26
27
  }
27
28
  export function readConfigFile(configPath) {
28
29
  try {
package/dist/install.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { type WarmApiAudience } from './config-paths.js';
1
+ import { type PrivateMcpMode } from '@warmio/contracts/mcp';
2
2
  export interface InstallOptions {
3
3
  force?: boolean;
4
- mode?: WarmApiAudience;
4
+ mode?: PrivateMcpMode;
5
5
  validateApiKey?: boolean;
6
6
  }
7
7
  export declare function install(options?: InstallOptions): Promise<void>;
package/dist/install.js CHANGED
@@ -2,6 +2,7 @@ import { createInterface } from 'readline';
2
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
3
  import { dirname, join, resolve } from 'path';
4
4
  import { homedir, platform } from 'os';
5
+ import { WARM_MCP_CLIENTS, WARM_MCP_CREDENTIALS, WARM_MCP_PROJECT_CONFIGS, WARM_MCP_SERVER_CONFIGS, } from '@warmio/contracts/mcp';
5
6
  import { verifyWarmApiKey, WarmApiError } from './server.js';
6
7
  import { getWarmApiKeyPath, readConfigFile } from './config-paths.js';
7
8
  const HOME = homedir();
@@ -9,56 +10,50 @@ const CWD = process.cwd();
9
10
  function isRecord(value) {
10
11
  return typeof value === 'object' && value !== null && !Array.isArray(value);
11
12
  }
12
- function getClaudeDesktopPath() {
13
- if (platform() === 'win32') {
14
- return join(process.env.APPDATA || join(HOME, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
15
- }
16
- if (platform() === 'darwin') {
17
- return join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
18
- }
19
- return join(HOME, '.config', 'claude', 'claude_desktop_config.json');
20
- }
21
- const GLOBAL_CLIENTS = [
22
- {
23
- name: 'Claude Code',
24
- configPath: join(HOME, '.claude.json'),
25
- format: 'json',
26
- alwaysInclude: true,
27
- },
28
- { name: 'Claude Desktop', configPath: getClaudeDesktopPath(), format: 'json' },
29
- { name: 'Cursor', configPath: join(HOME, '.cursor', 'mcp.json'), format: 'json' },
30
- {
31
- name: 'Windsurf',
32
- configPath: join(HOME, '.codeium', 'windsurf', 'mcp_config.json'),
33
- format: 'json',
34
- },
35
- {
36
- name: 'OpenCode',
37
- configPath: join(HOME, '.config', 'opencode', 'opencode.json'),
38
- format: 'json',
39
- },
40
- { name: 'Codex CLI', configPath: join(HOME, '.codex', 'config.toml'), format: 'toml' },
41
- {
42
- name: 'Antigravity',
43
- configPath: join(HOME, '.gemini', 'antigravity', 'mcp_config.json'),
44
- format: 'json',
45
- },
46
- { name: 'Gemini CLI', configPath: join(HOME, '.gemini', 'settings.json'), format: 'json' },
47
- ];
48
- const PROJECT_CONFIGS = ['.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json'];
49
- const MCP_PACKAGE_SPEC = '@warmio/mcp@latest';
13
+ function resolveClientConfigPath(configPath) {
14
+ const root = configPath.root === 'appData' ? process.env.APPDATA || join(HOME, 'AppData', 'Roaming') : HOME;
15
+ return join(root, ...configPath.segments);
16
+ }
17
+ function getClientConfigPath(clientId) {
18
+ const client = WARM_MCP_CLIENTS.find((candidate) => candidate.id === clientId);
19
+ if (!client) {
20
+ throw new Error(`Unsupported MCP client: ${clientId}`);
21
+ }
22
+ const currentPlatform = platform();
23
+ const configPath = currentPlatform === 'darwin'
24
+ ? client.configPaths.darwin
25
+ : currentPlatform === 'win32'
26
+ ? client.configPaths.win32
27
+ : client.configPaths.linux;
28
+ return resolveClientConfigPath(configPath);
29
+ }
30
+ const GLOBAL_CLIENTS = WARM_MCP_CLIENTS.map((client) => ({
31
+ name: client.name,
32
+ format: client.format,
33
+ configPath: getClientConfigPath(client.id),
34
+ alwaysInclude: 'alwaysInclude' in client && client.alwaysInclude,
35
+ }));
50
36
  function getServerName(mode) {
51
- return mode === 'automation' ? 'warm-automation' : 'warm';
37
+ return WARM_MCP_SERVER_CONFIGS[mode].serverName;
38
+ }
39
+ function getCredential(mode) {
40
+ return WARM_MCP_CREDENTIALS[mode];
41
+ }
42
+ function getOppositeCredential(mode) {
43
+ return WARM_MCP_CREDENTIALS[mode === 'automation' ? 'context' : 'automation'];
52
44
  }
53
45
  function getMcpConfig(mode) {
54
- const args = ['-y', MCP_PACKAGE_SPEC, 'mcp', '--mode', mode];
46
+ const serverConfig = WARM_MCP_SERVER_CONFIGS[mode];
55
47
  return platform() === 'win32'
56
- ? { command: 'cmd', args: ['/c', 'npx', ...args] }
57
- : { command: 'npx', args };
48
+ ? { command: 'cmd', args: ['/c', serverConfig.command, ...serverConfig.args] }
49
+ : { command: serverConfig.command, args: [...serverConfig.args] };
50
+ }
51
+ function formatTomlStringArray(values) {
52
+ return `[${values.map((value) => JSON.stringify(value)).join(', ')}]`;
58
53
  }
59
54
  function detectProjectClients() {
60
55
  const found = [];
61
- for (const name of PROJECT_CONFIGS) {
56
+ for (const name of WARM_MCP_PROJECT_CONFIGS) {
62
57
  const configPath = resolve(CWD, name);
63
58
  if (existsSync(configPath)) {
64
59
  found.push({
@@ -97,18 +92,21 @@ function isSupportedWarmInvocation(command, args, mode) {
97
92
  }
98
93
  function getJsonEnvStatus(server, mode) {
99
94
  const env = isRecord(server?.env) ? server.env : undefined;
100
- const apiKeyName = mode === 'automation' ? 'WARM_AUTOMATION_API_KEY' : 'WARM_CONTEXT_API_KEY';
101
- const apiKeyFileName = `${apiKeyName}_FILE`;
102
- const oppositeApiKeyName = mode === 'automation' ? 'WARM_CONTEXT_API_KEY' : 'WARM_AUTOMATION_API_KEY';
103
- const inlineApiKey = typeof env?.[apiKeyName] === 'string' && env[apiKeyName].trim()
104
- ? env[apiKeyName].trim()
105
- : null;
95
+ const credential = getCredential(mode);
96
+ const oppositeCredential = getOppositeCredential(mode);
97
+ const apiKeyName = credential.apiKeyEnv;
98
+ const apiKeyFileName = credential.apiKeyFileEnv;
99
+ const inlineApiKey = typeof env?.[apiKeyName] === 'string' && env[apiKeyName].trim() ? env[apiKeyName].trim() : null;
106
100
  return {
107
101
  apiKeyFileOverride: typeof env?.[apiKeyFileName] === 'string' && env[apiKeyFileName].trim().length > 0
108
102
  ? env[apiKeyFileName].trim()
109
103
  : null,
110
- hasIrrelevantCredential: ['WARM_API_KEY', 'WARM_API_KEY_FILE', oppositeApiKeyName, `${oppositeApiKeyName}_FILE`]
111
- .some((name) => typeof env?.[name] === 'string' && env[name].trim().length > 0),
104
+ hasIrrelevantCredential: [
105
+ 'WARM_API_KEY',
106
+ 'WARM_API_KEY_FILE',
107
+ oppositeCredential.apiKeyEnv,
108
+ oppositeCredential.apiKeyFileEnv,
109
+ ].some((name) => typeof env?.[name] === 'string' && env[name].trim().length > 0),
112
110
  inlineApiKey,
113
111
  };
114
112
  }
@@ -196,19 +194,23 @@ function getTomlEnvStatus(content, mode) {
196
194
  inlineApiKey: null,
197
195
  };
198
196
  }
199
- const envLines = envBlock.split(/\r?\n/).slice(1).filter((line) => line.trim().length > 0);
200
- const apiKeyName = mode === 'automation' ? 'WARM_AUTOMATION_API_KEY' : 'WARM_CONTEXT_API_KEY';
201
- const oppositeApiKeyName = mode === 'automation' ? 'WARM_CONTEXT_API_KEY' : 'WARM_AUTOMATION_API_KEY';
197
+ const envLines = envBlock
198
+ .split(/\r?\n/)
199
+ .slice(1)
200
+ .filter((line) => line.trim().length > 0);
201
+ const credential = getCredential(mode);
202
+ const oppositeCredential = getOppositeCredential(mode);
203
+ const apiKeyName = credential.apiKeyEnv;
202
204
  const inlineApiKey = getTomlStringValue(envBlock, apiKeyName)?.trim() || null;
203
- const apiKeyFile = getTomlStringValue(envBlock, `${apiKeyName}_FILE`);
205
+ const apiKeyFile = getTomlStringValue(envBlock, credential.apiKeyFileEnv);
204
206
  return {
205
207
  apiKeyFileOverride: typeof apiKeyFile === 'string' && apiKeyFile.trim() ? apiKeyFile.trim() : null,
206
208
  envLines,
207
209
  hasIrrelevantCredential: [
208
210
  'WARM_API_KEY',
209
211
  'WARM_API_KEY_FILE',
210
- oppositeApiKeyName,
211
- `${oppositeApiKeyName}_FILE`,
212
+ oppositeCredential.apiKeyEnv,
213
+ oppositeCredential.apiKeyFileEnv,
212
214
  ].some((name) => Boolean(getTomlStringValue(envBlock, name)?.trim())),
213
215
  inlineApiKey,
214
216
  };
@@ -268,14 +270,15 @@ function configureJson(client, mode, preserveSelectedKeyFile) {
268
270
  const preserveProjectCommand = client.isProjectLevel &&
269
271
  !!existingCommand &&
270
272
  isSupportedWarmInvocation(existingCommand, existingArgs, mode);
271
- const selectedKeyFile = mode === 'automation' ? 'WARM_AUTOMATION_API_KEY_FILE' : 'WARM_CONTEXT_API_KEY_FILE';
272
- const oppositeKeyFile = mode === 'automation' ? 'WARM_CONTEXT_API_KEY_FILE' : 'WARM_AUTOMATION_API_KEY_FILE';
273
+ const credential = getCredential(mode);
274
+ const oppositeCredential = getOppositeCredential(mode);
275
+ const selectedKeyFile = credential.apiKeyFileEnv;
273
276
  for (const name of [
274
277
  'WARM_API_KEY',
275
278
  'WARM_API_KEY_FILE',
276
- 'WARM_CONTEXT_API_KEY',
277
- 'WARM_AUTOMATION_API_KEY',
278
- oppositeKeyFile,
279
+ credential.apiKeyEnv,
280
+ oppositeCredential.apiKeyEnv,
281
+ oppositeCredential.apiKeyFileEnv,
279
282
  ]) {
280
283
  delete nextEnv[name];
281
284
  }
@@ -309,19 +312,21 @@ function configureToml(client, mode, preserveSelectedKeyFile) {
309
312
  }
310
313
  }
311
314
  const serverName = getServerName(mode);
312
- const tomlCommand = platform() === 'win32' ? 'cmd' : 'npx';
313
- const tomlArgs = platform() === 'win32'
314
- ? `["/c", "npx", "-y", "${MCP_PACKAGE_SPEC}", "mcp", "--mode", "${mode}"]`
315
- : `["-y", "${MCP_PACKAGE_SPEC}", "mcp", "--mode", "${mode}"]`;
315
+ const serverConfig = WARM_MCP_SERVER_CONFIGS[mode];
316
+ const tomlCommand = platform() === 'win32' ? 'cmd' : serverConfig.command;
317
+ const posixArgs = formatTomlStringArray(serverConfig.args);
318
+ const windowsArgs = formatTomlStringArray(['/c', serverConfig.command, ...serverConfig.args]);
319
+ const tomlArgs = platform() === 'win32' ? windowsArgs : posixArgs;
316
320
  const warmBlock = `[mcp_servers.${serverName}]${lineEnding}command = "${tomlCommand}"${lineEnding}args = ${tomlArgs}${lineEnding}`;
317
- const selectedKeyFile = mode === 'automation' ? 'WARM_AUTOMATION_API_KEY_FILE' : 'WARM_CONTEXT_API_KEY_FILE';
318
- const oppositeKeyFile = mode === 'automation' ? 'WARM_CONTEXT_API_KEY_FILE' : 'WARM_AUTOMATION_API_KEY_FILE';
321
+ const credential = getCredential(mode);
322
+ const oppositeCredential = getOppositeCredential(mode);
323
+ const selectedKeyFile = credential.apiKeyFileEnv;
319
324
  const removedEnvNames = new Set([
320
325
  'WARM_API_KEY',
321
326
  'WARM_API_KEY_FILE',
322
- 'WARM_CONTEXT_API_KEY',
323
- 'WARM_AUTOMATION_API_KEY',
324
- oppositeKeyFile,
327
+ credential.apiKeyEnv,
328
+ oppositeCredential.apiKeyEnv,
329
+ oppositeCredential.apiKeyFileEnv,
325
330
  ...(!preserveSelectedKeyFile ? [selectedKeyFile] : []),
326
331
  ]);
327
332
  const preservedEnvLines = getTomlEnvStatus(content, mode).envLines.filter((line) => {
@@ -452,8 +457,7 @@ export async function install(options = {}) {
452
457
  }
453
458
  const apiKeyPath = getWarmApiKeyPath(mode);
454
459
  const storedApiKey = readConfigFile(apiKeyPath);
455
- const migratedApiKey = needsSetup.find((status) => typeof status.inlineApiKey === 'string' && status.inlineApiKey.length > 0)
456
- ?.inlineApiKey ?? null;
460
+ const migratedApiKey = needsSetup.find((status) => typeof status.inlineApiKey === 'string' && status.inlineApiKey.length > 0)?.inlineApiKey ?? null;
457
461
  const usableApiKeyFileOverride = needsSetup.find((status) => status.apiKeyFileOverride && !status.inlineApiKey && status.credentialApiKey);
458
462
  const targetMissingCredential = needsSetup.some((status) => !status.credentialApiKey);
459
463
  let apiKeyToStore = null;
@@ -472,7 +476,7 @@ export async function install(options = {}) {
472
476
  shouldStoreApiKey = true;
473
477
  }
474
478
  else if (usableApiKeyFileOverride && !targetMissingCredential) {
475
- console.log(` Reusing existing ${mode === 'automation' ? 'WARM_AUTOMATION_API_KEY_FILE' : 'WARM_CONTEXT_API_KEY_FILE'} override.`);
479
+ console.log(` Reusing existing ${getCredential(mode).apiKeyFileEnv} override.`);
476
480
  console.log('');
477
481
  }
478
482
  else {