@warmio/mcp 8.0.0 → 9.1.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,56 +2,54 @@ 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
- import { WARM_MCP_CLIENTS, WARM_MCP_PACKAGE_SPEC, WARM_MCP_PROJECT_CONFIGS, } from './manifest.js';
8
8
  const HOME = homedir();
9
9
  const CWD = process.cwd();
10
10
  function isRecord(value) {
11
11
  return typeof value === 'object' && value !== null && !Array.isArray(value);
12
12
  }
13
- function getClaudeDesktopPath() {
14
- if (platform() === 'win32') {
15
- return join(process.env.APPDATA || join(HOME, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
16
- }
17
- if (platform() === 'darwin') {
18
- return join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
19
- }
20
- return join(HOME, '.config', 'claude', 'claude_desktop_config.json');
13
+ function resolveClientConfigPath(configPath) {
14
+ const root = configPath.root === 'appData' ? process.env.APPDATA || join(HOME, 'AppData', 'Roaming') : HOME;
15
+ return join(root, ...configPath.segments);
21
16
  }
22
17
  function getClientConfigPath(clientId) {
23
- switch (clientId) {
24
- case 'claude-code':
25
- return join(HOME, '.claude.json');
26
- case 'claude-desktop':
27
- return getClaudeDesktopPath();
28
- case 'cursor':
29
- return join(HOME, '.cursor', 'mcp.json');
30
- case 'windsurf':
31
- return join(HOME, '.codeium', 'windsurf', 'mcp_config.json');
32
- case 'opencode':
33
- return join(HOME, '.config', 'opencode', 'opencode.json');
34
- case 'codex-cli':
35
- return join(HOME, '.codex', 'config.toml');
36
- case 'antigravity':
37
- return join(HOME, '.gemini', 'antigravity', 'mcp_config.json');
38
- case 'gemini-cli':
39
- return join(HOME, '.gemini', 'settings.json');
40
- }
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);
41
29
  }
42
30
  const GLOBAL_CLIENTS = WARM_MCP_CLIENTS.map((client) => ({
43
- ...client,
31
+ name: client.name,
32
+ format: client.format,
44
33
  configPath: getClientConfigPath(client.id),
45
- alwaysInclude: client.id === 'claude-code',
34
+ alwaysInclude: 'alwaysInclude' in client && client.alwaysInclude,
46
35
  }));
47
36
  function getServerName(mode) {
48
- 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'];
49
44
  }
50
45
  function getMcpConfig(mode) {
51
- const args = ['-y', WARM_MCP_PACKAGE_SPEC, 'mcp', '--mode', mode];
46
+ const serverConfig = WARM_MCP_SERVER_CONFIGS[mode];
52
47
  return platform() === 'win32'
53
- ? { command: 'cmd', args: ['/c', 'npx', ...args] }
54
- : { 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(', ')}]`;
55
53
  }
56
54
  function detectProjectClients() {
57
55
  const found = [];
@@ -94,18 +92,21 @@ function isSupportedWarmInvocation(command, args, mode) {
94
92
  }
95
93
  function getJsonEnvStatus(server, mode) {
96
94
  const env = isRecord(server?.env) ? server.env : undefined;
97
- const apiKeyName = mode === 'automation' ? 'WARM_AUTOMATION_API_KEY' : 'WARM_CONTEXT_API_KEY';
98
- const apiKeyFileName = `${apiKeyName}_FILE`;
99
- const oppositeApiKeyName = mode === 'automation' ? 'WARM_CONTEXT_API_KEY' : 'WARM_AUTOMATION_API_KEY';
100
- const inlineApiKey = typeof env?.[apiKeyName] === 'string' && env[apiKeyName].trim()
101
- ? env[apiKeyName].trim()
102
- : 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;
103
100
  return {
104
101
  apiKeyFileOverride: typeof env?.[apiKeyFileName] === 'string' && env[apiKeyFileName].trim().length > 0
105
102
  ? env[apiKeyFileName].trim()
106
103
  : null,
107
- hasIrrelevantCredential: ['WARM_API_KEY', 'WARM_API_KEY_FILE', oppositeApiKeyName, `${oppositeApiKeyName}_FILE`]
108
- .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),
109
110
  inlineApiKey,
110
111
  };
111
112
  }
@@ -193,19 +194,23 @@ function getTomlEnvStatus(content, mode) {
193
194
  inlineApiKey: null,
194
195
  };
195
196
  }
196
- const envLines = envBlock.split(/\r?\n/).slice(1).filter((line) => line.trim().length > 0);
197
- const apiKeyName = mode === 'automation' ? 'WARM_AUTOMATION_API_KEY' : 'WARM_CONTEXT_API_KEY';
198
- 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;
199
204
  const inlineApiKey = getTomlStringValue(envBlock, apiKeyName)?.trim() || null;
200
- const apiKeyFile = getTomlStringValue(envBlock, `${apiKeyName}_FILE`);
205
+ const apiKeyFile = getTomlStringValue(envBlock, credential.apiKeyFileEnv);
201
206
  return {
202
207
  apiKeyFileOverride: typeof apiKeyFile === 'string' && apiKeyFile.trim() ? apiKeyFile.trim() : null,
203
208
  envLines,
204
209
  hasIrrelevantCredential: [
205
210
  'WARM_API_KEY',
206
211
  'WARM_API_KEY_FILE',
207
- oppositeApiKeyName,
208
- `${oppositeApiKeyName}_FILE`,
212
+ oppositeCredential.apiKeyEnv,
213
+ oppositeCredential.apiKeyFileEnv,
209
214
  ].some((name) => Boolean(getTomlStringValue(envBlock, name)?.trim())),
210
215
  inlineApiKey,
211
216
  };
@@ -265,14 +270,15 @@ function configureJson(client, mode, preserveSelectedKeyFile) {
265
270
  const preserveProjectCommand = client.isProjectLevel &&
266
271
  !!existingCommand &&
267
272
  isSupportedWarmInvocation(existingCommand, existingArgs, mode);
268
- const selectedKeyFile = mode === 'automation' ? 'WARM_AUTOMATION_API_KEY_FILE' : 'WARM_CONTEXT_API_KEY_FILE';
269
- 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;
270
276
  for (const name of [
271
277
  'WARM_API_KEY',
272
278
  'WARM_API_KEY_FILE',
273
- 'WARM_CONTEXT_API_KEY',
274
- 'WARM_AUTOMATION_API_KEY',
275
- oppositeKeyFile,
279
+ credential.apiKeyEnv,
280
+ oppositeCredential.apiKeyEnv,
281
+ oppositeCredential.apiKeyFileEnv,
276
282
  ]) {
277
283
  delete nextEnv[name];
278
284
  }
@@ -306,19 +312,21 @@ function configureToml(client, mode, preserveSelectedKeyFile) {
306
312
  }
307
313
  }
308
314
  const serverName = getServerName(mode);
309
- const tomlCommand = platform() === 'win32' ? 'cmd' : 'npx';
310
- const tomlArgs = platform() === 'win32'
311
- ? `["/c", "npx", "-y", "${WARM_MCP_PACKAGE_SPEC}", "mcp", "--mode", "${mode}"]`
312
- : `["-y", "${WARM_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;
313
320
  const warmBlock = `[mcp_servers.${serverName}]${lineEnding}command = "${tomlCommand}"${lineEnding}args = ${tomlArgs}${lineEnding}`;
314
- const selectedKeyFile = mode === 'automation' ? 'WARM_AUTOMATION_API_KEY_FILE' : 'WARM_CONTEXT_API_KEY_FILE';
315
- 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;
316
324
  const removedEnvNames = new Set([
317
325
  'WARM_API_KEY',
318
326
  'WARM_API_KEY_FILE',
319
- 'WARM_CONTEXT_API_KEY',
320
- 'WARM_AUTOMATION_API_KEY',
321
- oppositeKeyFile,
327
+ credential.apiKeyEnv,
328
+ oppositeCredential.apiKeyEnv,
329
+ oppositeCredential.apiKeyFileEnv,
322
330
  ...(!preserveSelectedKeyFile ? [selectedKeyFile] : []),
323
331
  ]);
324
332
  const preservedEnvLines = getTomlEnvStatus(content, mode).envLines.filter((line) => {
@@ -449,8 +457,7 @@ export async function install(options = {}) {
449
457
  }
450
458
  const apiKeyPath = getWarmApiKeyPath(mode);
451
459
  const storedApiKey = readConfigFile(apiKeyPath);
452
- const migratedApiKey = needsSetup.find((status) => typeof status.inlineApiKey === 'string' && status.inlineApiKey.length > 0)
453
- ?.inlineApiKey ?? null;
460
+ const migratedApiKey = needsSetup.find((status) => typeof status.inlineApiKey === 'string' && status.inlineApiKey.length > 0)?.inlineApiKey ?? null;
454
461
  const usableApiKeyFileOverride = needsSetup.find((status) => status.apiKeyFileOverride && !status.inlineApiKey && status.credentialApiKey);
455
462
  const targetMissingCredential = needsSetup.some((status) => !status.credentialApiKey);
456
463
  let apiKeyToStore = null;
@@ -469,7 +476,7 @@ export async function install(options = {}) {
469
476
  shouldStoreApiKey = true;
470
477
  }
471
478
  else if (usableApiKeyFileOverride && !targetMissingCredential) {
472
- 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.`);
473
480
  console.log('');
474
481
  }
475
482
  else {
package/dist/server.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type Implementation } from '@modelcontextprotocol/sdk/types.js';
3
+ import { type PrivateMcpMode } from '@warmio/contracts/mcp';
3
4
  import { createWarmApiClient, type WarmApiClientOptions, getConfiguredApiKey, verifyWarmApiKey, WarmApiError } from './warm-api-client.js';
4
5
  export { createWarmApiClient, getConfiguredApiKey, verifyWarmApiKey, WarmApiError };
5
6
  export type { WarmApiClientOptions } from './warm-api-client.js';
@@ -7,7 +8,7 @@ export interface WarmServerOptions extends WarmApiClientOptions {
7
8
  mode?: WarmServerMode;
8
9
  serverInfo?: Implementation;
9
10
  }
10
- export type WarmServerMode = 'automation' | 'context';
11
+ export type WarmServerMode = PrivateMcpMode;
11
12
  export declare const API_URL: string;
12
13
  export declare const WARM_SERVER_INFO: {
13
14
  name: string;
package/dist/types.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- export declare const WARM_CONTEXT_TOOL_NAMES: readonly ["get_financial_context", "get_transactions", "verify_key"];
2
- export declare const WARM_AUTOMATION_TOOL_NAMES: readonly ["search_operations", "describe_operation", "invoke_operation"];
3
- export declare const WARM_TOOL_NAMES: readonly ["get_financial_context", "get_transactions", "verify_key", "search_operations", "describe_operation", "invoke_operation"];
4
- export type WarmToolName = (typeof WARM_TOOL_NAMES)[number];
1
+ import { privateMcpToolDefinitions } from '@warmio/contracts/mcp';
2
+ export declare const WARM_CONTEXT_TOOL_NAMES: string[];
3
+ export declare const WARM_AUTOMATION_TOOL_NAMES: string[];
4
+ export declare const WARM_TOOL_NAMES: string[];
5
+ export type WarmToolName = (typeof privateMcpToolDefinitions)[number]['name'];
5
6
  export type { Account, AutomationInput, AutomationOperation, AutomationOperationDescription, Budget, DescribeOperationOutput, FinancialContext, FinancialContextMeta, GetTransactionsInput, GetTransactionsOutput, Goal, Health, Holding, InvokeOperationOutput, LatestTransactions, Liability, Position, Recurring, Snapshot, Status, Transaction, TransactionIndex, TransactionMonth, VerifyKeyOutput, WarmApiClient, WarmApiClientOptions, } from './warm-api-client.js';
6
- export type { WarmApiAudience } from './config-paths.js';
7
+ export type { PrivateMcpMode } from '@warmio/contracts/mcp';
7
8
  export type { WarmServerOptions } from './server.js';
package/dist/types.js CHANGED
@@ -1,11 +1,8 @@
1
- export const WARM_CONTEXT_TOOL_NAMES = [
2
- 'get_financial_context',
3
- 'get_transactions',
4
- 'verify_key',
5
- ];
6
- export const WARM_AUTOMATION_TOOL_NAMES = [
7
- 'search_operations',
8
- 'describe_operation',
9
- 'invoke_operation',
10
- ];
11
- export const WARM_TOOL_NAMES = [...WARM_CONTEXT_TOOL_NAMES, ...WARM_AUTOMATION_TOOL_NAMES];
1
+ import { privateMcpToolDefinitions } from '@warmio/contracts/mcp';
2
+ export const WARM_CONTEXT_TOOL_NAMES = privateMcpToolDefinitions
3
+ .filter((tool) => tool.mode === 'context')
4
+ .map((tool) => tool.name);
5
+ export const WARM_AUTOMATION_TOOL_NAMES = privateMcpToolDefinitions
6
+ .filter((tool) => tool.mode === 'automation')
7
+ .map((tool) => tool.name);
8
+ export const WARM_TOOL_NAMES = privateMcpToolDefinitions.map((tool) => tool.name);
@@ -1,8 +1,8 @@
1
- import { type WarmApiAudience } from './config-paths.js';
1
+ import { type PrivateMcpMode } from '@warmio/contracts/mcp';
2
2
  import type { AutomationInput, AutomationOperation, FinancialContext, FinancialContextAccount, FinancialContextBudget, FinancialContextGoal, FinancialContextHealth, FinancialContextHolding, FinancialContextLiability, FinancialContextMeta, FinancialContextPosition, FinancialContextRecurring, FinancialContextSnapshot, FinancialContextStatus, FinancialContextTransaction, FinancialContextTransactionIndex, LatestTransactions, TransactionMonth } from '@warmio/contracts/types';
3
3
  export type { AutomationInput, AutomationOperation, FinancialContext, FinancialContextMeta, LatestTransactions, TransactionMonth, } from '@warmio/contracts/types';
4
4
  export interface WarmApiClientOptions {
5
- audience?: WarmApiAudience;
5
+ audience?: PrivateMcpMode;
6
6
  apiKeyResolver?: () => string | null;
7
7
  apiUrl?: string;
8
8
  fetchImplementation?: typeof fetch;
@@ -63,7 +63,7 @@ export interface WarmApiClient {
63
63
  validateAudience(): Promise<VerifyKeyOutput>;
64
64
  verifyKey(): Promise<VerifyKeyOutput>;
65
65
  }
66
- export declare function getConfiguredApiKey(audience?: WarmApiAudience): string | null;
66
+ export declare function getConfiguredApiKey(audience?: PrivateMcpMode): string | null;
67
67
  export declare function resetConfiguredApiKeyCache(): void;
68
68
  interface WarmApiResponse<TBody> {
69
69
  body: TBody;
@@ -77,4 +77,4 @@ export declare class WarmApiError extends Error {
77
77
  constructor(message: string, response: WarmApiResponse<unknown>);
78
78
  }
79
79
  export declare function createWarmApiClient(options?: WarmApiClientOptions): WarmApiClient;
80
- export declare function verifyWarmApiKey(apiKey: string, audience?: WarmApiAudience): Promise<VerifyKeyOutput>;
80
+ export declare function verifyWarmApiKey(apiKey: string, audience?: PrivateMcpMode): Promise<VerifyKeyOutput>;
@@ -1,10 +1,20 @@
1
- import { getWarmApiKeyPath, readConfigFile, } from './config-paths.js';
2
- const DEFAULT_API_URL = process.env.WARM_API_URL || 'https://app.warm.io';
1
+ import { getWarmApiKeyPath, readConfigFile } from './config-paths.js';
2
+ import { API_ORIGIN, apiEndpointManifest, } from '@warmio/contracts/api';
3
+ import { WARM_MCP_CREDENTIALS, WARM_MCP_INSTALLER_COMMANDS, } from '@warmio/contracts/mcp';
4
+ const DEFAULT_API_URL = process.env.WARM_API_URL || API_ORIGIN;
3
5
  const DEFAULT_REQUEST_TIMEOUT_MS = (() => {
4
6
  const raw = Number(process.env.WARM_API_TIMEOUT_MS || 10_000);
5
7
  return Number.isFinite(raw) && raw > 0 ? raw : 10_000;
6
8
  })();
7
9
  const cachedApiKeys = new Map();
10
+ const endpointByOperationId = Object.fromEntries(apiEndpointManifest.map((endpoint) => [endpoint.operation_id, endpoint]));
11
+ function getEndpointPath(operationId, audience) {
12
+ const endpoint = endpointByOperationId[operationId];
13
+ if (endpoint.audience !== audience) {
14
+ throw new Error(`API operation ${operationId} is not available to ${audience}.`);
15
+ }
16
+ return endpoint.path;
17
+ }
8
18
  function getRequestSignal(timeoutMs) {
9
19
  if (typeof AbortSignal.timeout === 'function') {
10
20
  return AbortSignal.timeout(timeoutMs);
@@ -27,7 +37,7 @@ export function getConfiguredApiKey(audience = 'context') {
27
37
  if (cachedApiKeys.has(audience)) {
28
38
  return cachedApiKeys.get(audience) ?? null;
29
39
  }
30
- const environmentKey = audience === 'automation' ? 'WARM_AUTOMATION_API_KEY' : 'WARM_CONTEXT_API_KEY';
40
+ const environmentKey = WARM_MCP_CREDENTIALS[audience].apiKeyEnv;
31
41
  const apiKey = process.env[environmentKey]?.trim() || readConfigFile(getWarmApiKeyPath(audience));
32
42
  cachedApiKeys.set(audience, apiKey);
33
43
  return apiKey;
@@ -65,7 +75,7 @@ async function requestWarmApi(request, options) {
65
75
  const requestOptions = createWarmApiClientConfig(options);
66
76
  const apiKey = requestOptions.apiKeyResolver?.() ?? getConfiguredApiKey(requestOptions.audience);
67
77
  if (!apiKey) {
68
- throw new Error(`No ${requestOptions.audience} API key configured. Run "npx @warmio/mcp install --mode ${requestOptions.audience}" or set ${requestOptions.audience === 'automation' ? 'WARM_AUTOMATION_API_KEY' : 'WARM_CONTEXT_API_KEY'}.`);
78
+ throw new Error(`No ${requestOptions.audience} API key configured. Run "${WARM_MCP_INSTALLER_COMMANDS[requestOptions.audience]}" or set ${WARM_MCP_CREDENTIALS[requestOptions.audience].apiKeyEnv}.`);
69
79
  }
70
80
  const url = new URL(request.endpoint, requestOptions.apiUrl);
71
81
  Object.entries(request.query || {}).forEach(([key, value]) => {
@@ -119,23 +129,29 @@ async function requestSuccessfulJson(request, options) {
119
129
  return response.body;
120
130
  }
121
131
  export function createWarmApiClient(options = {}) {
122
- const getFinancialContext = async () => await requestSuccessfulJson({ endpoint: '/api/financial-context' }, options);
123
- const getFinancialContextMeta = async () => await requestSuccessfulJson({ endpoint: '/api/financial-context/meta' }, options);
132
+ const getFinancialContext = async () => await requestSuccessfulJson({ endpoint: getEndpointPath('getFinancialContext', 'context') }, options);
133
+ const getFinancialContextMeta = async () => await requestSuccessfulJson({ endpoint: getEndpointPath('getFinancialContextMeta', 'context') }, options);
124
134
  const getTransactions = async (input) => {
125
135
  const rawInput = input;
126
136
  if (typeof rawInput.month === 'string' && rawInput.latest !== undefined) {
127
137
  throw new Error('`month` and `latest` are mutually exclusive.');
128
138
  }
129
139
  if (typeof rawInput.month === 'string') {
130
- return await requestSuccessfulJson({ endpoint: '/api/financial-context/transactions', query: { month: rawInput.month } }, options);
140
+ return await requestSuccessfulJson({
141
+ endpoint: getEndpointPath('getFinancialContextTransactions', 'context'),
142
+ query: { month: rawInput.month },
143
+ }, options);
131
144
  }
132
145
  if (rawInput.latest === true) {
133
- return await requestSuccessfulJson({ endpoint: '/api/financial-context/transactions', query: { latest: '1' } }, options);
146
+ return await requestSuccessfulJson({
147
+ endpoint: getEndpointPath('getFinancialContextTransactions', 'context'),
148
+ query: { latest: '1' },
149
+ }, options);
134
150
  }
135
151
  throw new Error('Call getTransactions with `month` in YYYY-MM format or `latest: true`.');
136
152
  };
137
153
  const verifyKey = async () => {
138
- const response = await requestSuccessfulJson({ endpoint: '/api/verify' }, options);
154
+ const response = await requestSuccessfulJson({ endpoint: getEndpointPath('verifyApiAccess', 'context') }, options);
139
155
  return {
140
156
  audience: response.audience,
141
157
  status: response.status || (response.valid ? 'ok' : 'invalid'),
@@ -147,14 +163,18 @@ export function createWarmApiClient(options = {}) {
147
163
  const expectedAudience = createWarmApiClientConfig(options).audience;
148
164
  if (!result.valid || result.audience !== expectedAudience) {
149
165
  const article = expectedAudience === 'automation' ? 'an' : 'a';
150
- throw new Error(`This MCP mode requires ${article} ${expectedAudience} API key. Run "npx @warmio/mcp install --mode ${expectedAudience}" to configure a separate key.`);
166
+ throw new Error(`This MCP mode requires ${article} ${expectedAudience} API key. Run "${WARM_MCP_INSTALLER_COMMANDS[expectedAudience]}" to configure a separate key.`);
151
167
  }
152
168
  return result;
153
169
  };
154
- const searchOperations = async (query) => await requestSuccessfulJson({ body: query ? { query } : {}, endpoint: '/api/automation/operations/search', method: 'POST' }, options);
170
+ const searchOperations = async (query) => await requestSuccessfulJson({
171
+ body: query ? { query } : {},
172
+ endpoint: getEndpointPath('searchOperations', 'automation'),
173
+ method: 'POST',
174
+ }, options);
155
175
  const describeOperation = async (operationId, input) => await requestSuccessfulJson({
156
176
  body: { operation_id: operationId, ...(input ? { input } : {}) },
157
- endpoint: '/api/automation/operations/describe',
177
+ endpoint: getEndpointPath('describeOperation', 'automation'),
158
178
  method: 'POST',
159
179
  }, options);
160
180
  const invokeOperation = async (operationId, input, approvalId) => await requestSuccessfulJson({
@@ -163,7 +183,7 @@ export function createWarmApiClient(options = {}) {
163
183
  input: input ?? {},
164
184
  ...(approvalId ? { approval_id: approvalId } : {}),
165
185
  },
166
- endpoint: '/api/automation/operations/invoke',
186
+ endpoint: getEndpointPath('invokeOperation', 'automation'),
167
187
  method: 'POST',
168
188
  }, options);
169
189
  return {
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@warmio/mcp",
3
- "version": "8.0.0",
3
+ "version": "9.1.0",
4
4
  "mcpName": "io.github.jasonstockman/warmio",
5
5
  "description": "Warm CLI and compact context or automation MCP server",
6
6
  "type": "module",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
7
10
  "main": "dist/index.js",
8
11
  "types": "dist/index.d.ts",
9
12
  "publishConfig": {
@@ -25,10 +28,6 @@
25
28
  "./install": {
26
29
  "types": "./dist/install.d.ts",
27
30
  "default": "./dist/install.js"
28
- },
29
- "./manifest": {
30
- "types": "./dist/manifest.d.ts",
31
- "default": "./dist/manifest.js"
32
31
  }
33
32
  },
34
33
  "bin": {
@@ -38,7 +37,7 @@
38
37
  "dist"
39
38
  ],
40
39
  "scripts": {
41
- "build": "tsc",
40
+ "build": "node scripts/clean.mjs && tsc",
42
41
  "test": "npm run build && node --test test/*.mjs",
43
42
  "start": "node dist/index.js mcp",
44
43
  "start:http": "node dist/index.js http",
@@ -61,7 +60,7 @@
61
60
  "url": "git+https://github.com/jasonstockman/warm-mcp.git"
62
61
  },
63
62
  "dependencies": {
64
- "@warmio/contracts": "1.0.0",
63
+ "@warmio/contracts": "1.2.0",
65
64
  "@modelcontextprotocol/sdk": "^1.27.1",
66
65
  "dotenv": "^17.2.3",
67
66
  "zod": "^3.25.76"
@@ -1,85 +0,0 @@
1
- export declare const WARM_MCP_PACKAGE_NAME: "@warmio/mcp";
2
- export declare const WARM_MCP_PACKAGE_SPEC: "@warmio/mcp@latest";
3
- export declare const WARM_MCP_INSTALL_COMMAND: "npx -y @warmio/mcp@latest";
4
- export declare const WARM_MCP_CLIENTS: readonly [{
5
- readonly id: "claude-code";
6
- readonly name: "Claude Code";
7
- readonly format: "json";
8
- }, {
9
- readonly id: "claude-desktop";
10
- readonly name: "Claude Desktop";
11
- readonly format: "json";
12
- }, {
13
- readonly id: "cursor";
14
- readonly name: "Cursor";
15
- readonly format: "json";
16
- }, {
17
- readonly id: "windsurf";
18
- readonly name: "Windsurf";
19
- readonly format: "json";
20
- }, {
21
- readonly id: "opencode";
22
- readonly name: "OpenCode";
23
- readonly format: "json";
24
- }, {
25
- readonly id: "codex-cli";
26
- readonly name: "Codex CLI";
27
- readonly format: "toml";
28
- }, {
29
- readonly id: "antigravity";
30
- readonly name: "Antigravity";
31
- readonly format: "json";
32
- }, {
33
- readonly id: "gemini-cli";
34
- readonly name: "Gemini CLI";
35
- readonly format: "json";
36
- }];
37
- export type WarmMcpClientId = (typeof WARM_MCP_CLIENTS)[number]['id'];
38
- export declare const WARM_MCP_PROJECT_CONFIGS: readonly [".mcp.json", ".cursor/mcp.json", ".vscode/mcp.json"];
39
- export declare const WARM_MCP_SERVER_COMMANDS: {
40
- readonly automation: "npx -y @warmio/mcp@latest mcp --mode automation";
41
- readonly context: "npx -y @warmio/mcp@latest mcp --mode context";
42
- };
43
- export declare const WARM_MCP_MANIFEST: {
44
- readonly clients: readonly [{
45
- readonly id: "claude-code";
46
- readonly name: "Claude Code";
47
- readonly format: "json";
48
- }, {
49
- readonly id: "claude-desktop";
50
- readonly name: "Claude Desktop";
51
- readonly format: "json";
52
- }, {
53
- readonly id: "cursor";
54
- readonly name: "Cursor";
55
- readonly format: "json";
56
- }, {
57
- readonly id: "windsurf";
58
- readonly name: "Windsurf";
59
- readonly format: "json";
60
- }, {
61
- readonly id: "opencode";
62
- readonly name: "OpenCode";
63
- readonly format: "json";
64
- }, {
65
- readonly id: "codex-cli";
66
- readonly name: "Codex CLI";
67
- readonly format: "toml";
68
- }, {
69
- readonly id: "antigravity";
70
- readonly name: "Antigravity";
71
- readonly format: "json";
72
- }, {
73
- readonly id: "gemini-cli";
74
- readonly name: "Gemini CLI";
75
- readonly format: "json";
76
- }];
77
- readonly installCommand: "npx -y @warmio/mcp@latest";
78
- readonly packageName: "@warmio/mcp";
79
- readonly packageSpec: "@warmio/mcp@latest";
80
- readonly projectConfigs: readonly [".mcp.json", ".cursor/mcp.json", ".vscode/mcp.json"];
81
- readonly serverCommands: {
82
- readonly automation: "npx -y @warmio/mcp@latest mcp --mode automation";
83
- readonly context: "npx -y @warmio/mcp@latest mcp --mode context";
84
- };
85
- };
package/dist/manifest.js DELETED
@@ -1,30 +0,0 @@
1
- export const WARM_MCP_PACKAGE_NAME = '@warmio/mcp';
2
- export const WARM_MCP_PACKAGE_SPEC = `${WARM_MCP_PACKAGE_NAME}@latest`;
3
- export const WARM_MCP_INSTALL_COMMAND = `npx -y ${WARM_MCP_PACKAGE_SPEC}`;
4
- export const WARM_MCP_CLIENTS = [
5
- { id: 'claude-code', name: 'Claude Code', format: 'json' },
6
- { id: 'claude-desktop', name: 'Claude Desktop', format: 'json' },
7
- { id: 'cursor', name: 'Cursor', format: 'json' },
8
- { id: 'windsurf', name: 'Windsurf', format: 'json' },
9
- { id: 'opencode', name: 'OpenCode', format: 'json' },
10
- { id: 'codex-cli', name: 'Codex CLI', format: 'toml' },
11
- { id: 'antigravity', name: 'Antigravity', format: 'json' },
12
- { id: 'gemini-cli', name: 'Gemini CLI', format: 'json' },
13
- ];
14
- export const WARM_MCP_PROJECT_CONFIGS = [
15
- '.mcp.json',
16
- '.cursor/mcp.json',
17
- '.vscode/mcp.json',
18
- ];
19
- export const WARM_MCP_SERVER_COMMANDS = {
20
- automation: `${WARM_MCP_INSTALL_COMMAND} mcp --mode automation`,
21
- context: `${WARM_MCP_INSTALL_COMMAND} mcp --mode context`,
22
- };
23
- export const WARM_MCP_MANIFEST = {
24
- clients: WARM_MCP_CLIENTS,
25
- installCommand: WARM_MCP_INSTALL_COMMAND,
26
- packageName: WARM_MCP_PACKAGE_NAME,
27
- packageSpec: WARM_MCP_PACKAGE_SPEC,
28
- projectConfigs: WARM_MCP_PROJECT_CONFIGS,
29
- serverCommands: WARM_MCP_SERVER_COMMANDS,
30
- };