@warmio/mcp 4.2.2 → 4.3.2

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
@@ -2,18 +2,22 @@
2
2
 
3
3
  Read-only MCP server for Warm financial data.
4
4
 
5
- This package supports local `stdio` transport only.
5
+ This package starts in local `stdio` mode by default and also supports optional Streamable HTTP.
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npx @warmio/mcp
10
+ npx -y @warmio/mcp@latest
11
11
  ```
12
12
 
13
13
  The installer detects supported MCP clients, prompts for your Warm API key, and writes the local
14
14
  `stdio` server config automatically. The key is stored once in your local Warm profile instead of
15
15
  being duplicated into every MCP client config.
16
16
 
17
+ Use `@latest` when you want the newest published package. Bare `npx @warmio/mcp` can reuse a
18
+ cached or local copy and leave you on an older version. If you want a deterministic install,
19
+ replace `@latest` with an exact version such as `@warmio/mcp@4.3.2`.
20
+
17
21
  When launched without arguments in a non-interactive context, the binary automatically starts the
18
22
  `stdio` server instead of the installer. This keeps MCP clients that invoke the package directly
19
23
  from falling into the interactive setup flow.
@@ -34,12 +38,15 @@ stay secret-free:
34
38
  "mcpServers": {
35
39
  "warm": {
36
40
  "command": "npx",
37
- "args": ["-y", "@warmio/mcp", "--server"]
41
+ "args": ["-y", "@warmio/mcp@latest", "--server"]
38
42
  }
39
43
  }
40
44
  }
41
45
  ```
42
46
 
47
+ If you already have a client config that uses bare `@warmio/mcp`, update it to `@warmio/mcp@latest`
48
+ or an exact version and restart the client.
49
+
43
50
  Optional auth overrides:
44
51
 
45
52
  - `WARM_API_KEY`
@@ -52,12 +59,27 @@ On Windows, prefer:
52
59
  "mcpServers": {
53
60
  "warm": {
54
61
  "command": "cmd",
55
- "args": ["/c", "npx", "-y", "@warmio/mcp", "--server"]
62
+ "args": ["/c", "npx", "-y", "@warmio/mcp@latest", "--server"]
56
63
  }
57
64
  }
58
65
  }
59
66
  ```
60
67
 
68
+ ## Optional Streamable HTTP
69
+
70
+ If your MCP client supports Streamable HTTP, you can run:
71
+
72
+ ```bash
73
+ npx -y @warmio/mcp@latest http --host 127.0.0.1 --port 3000 --path /mcp
74
+ ```
75
+
76
+ Relevant optional environment variables:
77
+
78
+ - `WARM_MCP_HTTP_HOST`
79
+ - `WARM_MCP_HTTP_PORT`
80
+ - `WARM_MCP_HTTP_PATH`
81
+ - `WARM_MCP_ALLOWED_HOSTS`
82
+
61
83
  ## Core Tools
62
84
 
63
85
  Warm's published/documented MCP surface is the following four-tool core:
@@ -1,2 +1,3 @@
1
1
  export declare function getWarmConfigDir(): string;
2
2
  export declare function getWarmApiKeyPath(): string;
3
+ export declare function readConfigFile(configPath: string): string | null;
@@ -1,3 +1,4 @@
1
+ import * as fs from 'node:fs';
1
2
  import * as os from 'node:os';
2
3
  import * as path from 'node:path';
3
4
  export function getWarmConfigDir() {
@@ -21,3 +22,11 @@ export function getWarmApiKeyPath() {
21
22
  }
22
23
  return path.join(getWarmConfigDir(), 'api_key');
23
24
  }
25
+ export function readConfigFile(configPath) {
26
+ try {
27
+ return fs.readFileSync(configPath, 'utf-8').trim() || null;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
package/dist/http.d.ts CHANGED
@@ -1,9 +1,8 @@
1
- import type { Server as HttpServer } from 'http';
2
- export interface WarmHttpServerOptions {
1
+ export interface HttpServerOptions {
2
+ allowedHosts?: string[];
3
3
  host?: string;
4
- port?: number;
5
4
  path?: string;
6
- allowedHosts?: string[];
5
+ port?: number;
7
6
  }
8
- export declare function resolveHttpServerOptions(overrides?: WarmHttpServerOptions): Required<WarmHttpServerOptions>;
9
- export declare function startHttpServer(options?: WarmHttpServerOptions): Promise<HttpServer>;
7
+ export declare function resolveHttpServerOptions(overrides?: HttpServerOptions): Required<HttpServerOptions>;
8
+ export declare function startHttpServer(options?: HttpServerOptions): Promise<any>;
package/dist/http.js CHANGED
@@ -25,36 +25,38 @@ function parseAllowedHosts(value) {
25
25
  }
26
26
  function jsonRpcError(message) {
27
27
  return {
28
- jsonrpc: '2.0',
29
28
  error: {
30
29
  code: -32000,
31
30
  message,
32
31
  },
33
32
  id: null,
33
+ jsonrpc: '2.0',
34
34
  };
35
35
  }
36
- function sendMethodNotAllowed(res) {
37
- res.status(405).set('Allow', 'POST').json(jsonRpcError('Method not allowed.'));
38
- }
39
36
  export function resolveHttpServerOptions(overrides = {}) {
40
37
  return {
38
+ allowedHosts: overrides.allowedHosts ??
39
+ parseAllowedHosts(process.env.WARM_MCP_ALLOWED_HOSTS || process.env.MCP_ALLOWED_HOSTS),
41
40
  host: overrides.host ||
42
41
  process.env.WARM_MCP_HTTP_HOST ||
43
42
  process.env.MCP_HOST ||
44
43
  process.env.HOST ||
45
44
  DEFAULT_HTTP_HOST,
45
+ path: normalizePath(overrides.path || process.env.WARM_MCP_HTTP_PATH || process.env.MCP_PATH || DEFAULT_HTTP_PATH),
46
46
  port: overrides.port ??
47
47
  parsePort(process.env.WARM_MCP_HTTP_PORT || process.env.MCP_PORT || process.env.PORT, DEFAULT_HTTP_PORT),
48
- path: normalizePath(overrides.path || process.env.WARM_MCP_HTTP_PATH || process.env.MCP_PATH || DEFAULT_HTTP_PATH),
49
- allowedHosts: overrides.allowedHosts ??
50
- parseAllowedHosts(process.env.WARM_MCP_ALLOWED_HOSTS || process.env.MCP_ALLOWED_HOSTS),
51
48
  };
52
49
  }
50
+ function sendJsonMethodNotAllowed(res) {
51
+ res.status(405);
52
+ res.set('Allow', 'POST');
53
+ res.json(jsonRpcError('Method not allowed.'));
54
+ }
53
55
  export async function startHttpServer(options = {}) {
54
56
  const resolved = resolveHttpServerOptions(options);
55
57
  const app = createMcpExpressApp({
56
- host: resolved.host,
57
58
  ...(resolved.allowedHosts.length > 0 ? { allowedHosts: resolved.allowedHosts } : {}),
59
+ host: resolved.host,
58
60
  });
59
61
  app.post(resolved.path, async (req, res) => {
60
62
  const server = createWarmServer();
@@ -85,10 +87,10 @@ export async function startHttpServer(options = {}) {
85
87
  }
86
88
  });
87
89
  app.get(resolved.path, (_req, res) => {
88
- sendMethodNotAllowed(res);
90
+ sendJsonMethodNotAllowed(res);
89
91
  });
90
92
  app.delete(resolved.path, (_req, res) => {
91
- sendMethodNotAllowed(res);
93
+ sendJsonMethodNotAllowed(res);
92
94
  });
93
95
  const listener = await new Promise((resolve, reject) => {
94
96
  const httpServer = app.listen(resolved.port, resolved.host, () => {
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ export declare function startStdioServer(): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { install } from './install.js';
3
- import { startStdioServer } from './server.js';
3
+ import { startHttpServer } from './http.js';
4
+ import { loadDotEnv } from './load-env.js';
5
+ import { createWarmServer } from './server.js';
6
+ import { AdaptiveStdioTransport } from './stdio.js';
4
7
  function isInteractiveSession() {
5
8
  return process.stdin.isTTY === true && process.stdout.isTTY === true;
6
9
  }
@@ -15,6 +18,7 @@ function printUsage() {
15
18
  console.log('');
16
19
  console.log(' warm-mcp install [--force] [--no-validate]');
17
20
  console.log(' warm-mcp stdio');
21
+ console.log(' warm-mcp http [--host 127.0.0.1] [--port 3000] [--path /mcp]');
18
22
  console.log('');
19
23
  console.log(' Aliases:');
20
24
  console.log(' --server, --stdio Start stdio mode');
@@ -40,6 +44,33 @@ function parseCliArgs(args) {
40
44
  options.command = 'stdio';
41
45
  continue;
42
46
  }
47
+ if (arg === 'http') {
48
+ options.command = 'http';
49
+ continue;
50
+ }
51
+ if (arg === '--host') {
52
+ options.httpHost = args[index + 1];
53
+ index += 1;
54
+ continue;
55
+ }
56
+ if (arg === '--path') {
57
+ options.httpPath = args[index + 1];
58
+ index += 1;
59
+ continue;
60
+ }
61
+ if (arg === '--port') {
62
+ const nextArg = args[index + 1];
63
+ if (!nextArg) {
64
+ throw new Error('Missing value for --port');
65
+ }
66
+ const port = Number.parseInt(nextArg, 10);
67
+ if (!Number.isInteger(port) || port <= 0) {
68
+ throw new Error(`Invalid value for --port: ${nextArg}`);
69
+ }
70
+ options.httpPort = port;
71
+ index += 1;
72
+ continue;
73
+ }
43
74
  if (arg === '--force') {
44
75
  options.force = true;
45
76
  continue;
@@ -52,7 +83,14 @@ function parseCliArgs(args) {
52
83
  }
53
84
  return options;
54
85
  }
86
+ export async function startStdioServer() {
87
+ const server = createWarmServer();
88
+ const transport = new AdaptiveStdioTransport();
89
+ await server.connect(transport);
90
+ return server;
91
+ }
55
92
  async function main() {
93
+ loadDotEnv();
56
94
  const options = parseCliArgs(process.argv.slice(2));
57
95
  switch (options.command) {
58
96
  case 'help':
@@ -61,6 +99,13 @@ async function main() {
61
99
  case 'stdio':
62
100
  await startStdioServer();
63
101
  return;
102
+ case 'http':
103
+ await startHttpServer({
104
+ host: options.httpHost,
105
+ path: options.httpPath,
106
+ port: options.httpPort,
107
+ });
108
+ return;
64
109
  case 'install':
65
110
  await install({
66
111
  force: options.force,
@@ -73,8 +118,7 @@ try {
73
118
  await main();
74
119
  }
75
120
  catch (error) {
76
- const message = error instanceof Error ? error.message : String(error);
121
+ const message = error instanceof Error ? error.stack || error.message : String(error);
77
122
  console.error(message);
78
- console.error('Run "warm-mcp --help" for usage.');
79
123
  process.exit(1);
80
124
  }
package/dist/install.js CHANGED
@@ -3,7 +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
+ import { getWarmApiKeyPath, readConfigFile } from './config-paths.js';
7
7
  const HOME = homedir();
8
8
  const CWD = process.cwd();
9
9
  function isRecord(value) {
@@ -46,10 +46,13 @@ const GLOBAL_CLIENTS = [
46
46
  { name: 'Gemini CLI', configPath: join(HOME, '.gemini', 'settings.json'), format: 'json' },
47
47
  ];
48
48
  const PROJECT_CONFIGS = ['.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json'];
49
+ const MCP_PACKAGE_SPEC = '@warmio/mcp@latest';
49
50
  const MCP_CONFIG = platform() === 'win32'
50
- ? { command: 'cmd', args: ['/c', 'npx', '-y', '@warmio/mcp', '--server'] }
51
- : { command: 'npx', args: ['-y', '@warmio/mcp', '--server'] };
51
+ ? { command: 'cmd', args: ['/c', 'npx', '-y', MCP_PACKAGE_SPEC, '--server'] }
52
+ : { command: 'npx', args: ['-y', MCP_PACKAGE_SPEC, '--server'] };
52
53
  const WARM_API_KEY_PATH = getWarmApiKeyPath();
54
+ const WARM_TOML_BLOCK_PATTERN = /\n?\[mcp_servers\.warm\][\s\S]*?(?=\n\[[^\n]+\]|\s*$)/g;
55
+ const WARM_TOML_ENV_BLOCK_PATTERN = /\n?\[mcp_servers\.warm\.env\][\s\S]*?(?=\n\[[^\n]+\]|\s*$)/g;
53
56
  function detectProjectClients() {
54
57
  const found = [];
55
58
  for (const name of PROJECT_CONFIGS) {
@@ -71,20 +74,133 @@ function isDetected(client) {
71
74
  }
72
75
  return existsSync(dirname(client.configPath));
73
76
  }
74
- function isConfigured(client) {
75
- if (!existsSync(client.configPath)) {
76
- return false;
77
+ function getStringArray(value) {
78
+ if (!Array.isArray(value)) {
79
+ return [];
77
80
  }
81
+ return value.filter((entry) => typeof entry === 'string');
82
+ }
83
+ function isWarmPackageSpecifier(value) {
84
+ return /^@warmio\/mcp(?:@.+)?$/.test(value);
85
+ }
86
+ function isLegacyWarmPackageSpecifier(value) {
87
+ return /^@anthropic\/warm-mcp-server(?:@.+)?$/.test(value);
88
+ }
89
+ function normalizePathLikeValue(value) {
90
+ return value.replace(/\\/g, '/').toLowerCase();
91
+ }
92
+ function isLocalWarmMcpPath(value) {
93
+ const normalized = normalizePathLikeValue(value);
94
+ return normalized.includes('/warm-mcp/dist/index.js') || normalized.endsWith('/warm-mcp');
95
+ }
96
+ function isSupportedWarmInvocation(command, args) {
97
+ return [command, ...args].some((value) => typeof value === 'string' &&
98
+ (isWarmPackageSpecifier(value) || isLocalWarmMcpPath(value)));
99
+ }
100
+ function isLegacyWarmInvocation(command, args) {
101
+ return [command, ...args].some((value) => typeof value === 'string' && isLegacyWarmPackageSpecifier(value));
102
+ }
103
+ function getJsonEnvStatus(server) {
104
+ const env = isRecord(server?.env) ? server.env : undefined;
105
+ const inlineApiKey = typeof env?.WARM_API_KEY === 'string' && env.WARM_API_KEY.trim()
106
+ ? env.WARM_API_KEY.trim()
107
+ : null;
108
+ return {
109
+ hasApiKeyFileOverride: typeof env?.WARM_API_KEY_FILE === 'string' && env.WARM_API_KEY_FILE.trim().length > 0,
110
+ inlineApiKey,
111
+ };
112
+ }
113
+ function getJsonStatus(client, content) {
78
114
  try {
79
- const content = readFileSync(client.configPath, 'utf-8');
80
- if (client.format === 'toml') {
81
- return content.includes('[mcp_servers.warm]');
115
+ const parsed = JSON.parse(content);
116
+ const servers = isRecord(parsed.mcpServers) ? parsed.mcpServers : undefined;
117
+ const server = isRecord(servers?.warm) ? servers.warm : undefined;
118
+ const envStatus = getJsonEnvStatus(server);
119
+ if (!server) {
120
+ return { client, state: 'missing', ...envStatus };
121
+ }
122
+ const command = typeof server.command === 'string' ? server.command : undefined;
123
+ const args = getStringArray(server.args);
124
+ if (client.isProjectLevel && command && !isLegacyWarmInvocation(command, args)) {
125
+ return { client, state: 'configured', ...envStatus };
82
126
  }
83
- return !!JSON.parse(content)?.mcpServers?.warm;
127
+ if (isSupportedWarmInvocation(command, args)) {
128
+ return { client, state: 'configured', ...envStatus };
129
+ }
130
+ return { client, state: 'needs_migration', ...envStatus };
84
131
  }
85
132
  catch {
86
- return false;
133
+ return { client, state: 'missing', hasApiKeyFileOverride: false, inlineApiKey: null };
134
+ }
135
+ }
136
+ function escapeRegExp(value) {
137
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
138
+ }
139
+ function getTomlSection(content, sectionName) {
140
+ const pattern = new RegExp(`(?:^|\\n)\\[${escapeRegExp(sectionName)}\\]\\n([\\s\\S]*?)(?=\\n\\[[^\\n]+\\]|\\s*$)`);
141
+ const match = content.match(pattern);
142
+ return match ? match[0].replace(/^\n/, '') : null;
143
+ }
144
+ function getTomlStringValue(block, key) {
145
+ if (!block) {
146
+ return undefined;
147
+ }
148
+ const pattern = new RegExp(`^${escapeRegExp(key)}\\s*=\\s*(?:"([^"]*)"|'([^']*)')\\s*$`, 'm');
149
+ const match = block.match(pattern);
150
+ return match?.[1] ?? match?.[2];
151
+ }
152
+ function getTomlStringArrayValue(block, key) {
153
+ if (!block) {
154
+ return [];
155
+ }
156
+ const pattern = new RegExp(`^${escapeRegExp(key)}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s*$`, 'm');
157
+ const match = block.match(pattern);
158
+ if (!match) {
159
+ return [];
160
+ }
161
+ const values = [];
162
+ const valuePattern = /"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'/g;
163
+ for (const item of match[1].matchAll(valuePattern)) {
164
+ values.push(item[1] ?? item[2] ?? '');
165
+ }
166
+ return values;
167
+ }
168
+ function getTomlEnvStatus(content) {
169
+ const envBlock = getTomlSection(content, 'mcp_servers.warm.env');
170
+ if (!envBlock) {
171
+ return { envLines: [], hasApiKeyFileOverride: false, inlineApiKey: null };
172
+ }
173
+ const envLines = envBlock.split('\n').slice(1).filter((line) => line.trim().length > 0);
174
+ const inlineApiKey = getTomlStringValue(envBlock, 'WARM_API_KEY')?.trim() || null;
175
+ const apiKeyFile = getTomlStringValue(envBlock, 'WARM_API_KEY_FILE');
176
+ return {
177
+ envLines,
178
+ hasApiKeyFileOverride: typeof apiKeyFile === 'string' && apiKeyFile.trim().length > 0,
179
+ inlineApiKey,
180
+ };
181
+ }
182
+ function getTomlStatus(client, content) {
183
+ const warmBlock = getTomlSection(content, 'mcp_servers.warm');
184
+ const envStatus = getTomlEnvStatus(content);
185
+ if (!warmBlock) {
186
+ return { client, state: 'missing', ...envStatus };
187
+ }
188
+ const command = getTomlStringValue(warmBlock, 'command');
189
+ const args = getTomlStringArrayValue(warmBlock, 'args');
190
+ if (isSupportedWarmInvocation(command, args)) {
191
+ return { client, state: 'configured', ...envStatus };
192
+ }
193
+ return { client, state: 'needs_migration', ...envStatus };
194
+ }
195
+ function getClientStatus(client) {
196
+ if (!existsSync(client.configPath)) {
197
+ return { client, state: 'missing', hasApiKeyFileOverride: false, inlineApiKey: null };
87
198
  }
199
+ const content = readFileSync(client.configPath, 'utf-8');
200
+ if (client.format === 'toml') {
201
+ return getTomlStatus(client, content);
202
+ }
203
+ return getJsonStatus(client, content);
88
204
  }
89
205
  function configureJson(client) {
90
206
  let config = {};
@@ -103,8 +219,14 @@ function configureJson(client) {
103
219
  const existing = isRecord(servers.warm) ? servers.warm : undefined;
104
220
  const existingEnv = isRecord(existing?.env) ? existing.env : {};
105
221
  const nextEnv = { ...existingEnv };
222
+ const existingCommand = typeof existing?.command === 'string' ? existing.command : undefined;
223
+ const existingArgs = getStringArray(existing?.args);
224
+ const preserveProjectCommand = client.isProjectLevel &&
225
+ !!existingCommand &&
226
+ !isLegacyWarmInvocation(existingCommand, existingArgs) &&
227
+ !isWarmPackageSpecifier(existingCommand);
106
228
  delete nextEnv.WARM_API_KEY;
107
- if (client.isProjectLevel && existing?.command) {
229
+ if (preserveProjectCommand) {
108
230
  servers.warm = {
109
231
  ...existing,
110
232
  ...(Object.keys(nextEnv).length > 0 ? { env: nextEnv } : {}),
@@ -130,15 +252,21 @@ function configureToml(client) {
130
252
  }
131
253
  const tomlCommand = platform() === 'win32' ? 'cmd' : 'npx';
132
254
  const tomlArgs = platform() === 'win32'
133
- ? '["/c", "npx", "-y", "@warmio/mcp", "--server"]'
134
- : '["-y", "@warmio/mcp", "--server"]';
255
+ ? `["/c", "npx", "-y", "${MCP_PACKAGE_SPEC}", "--server"]`
256
+ : `["-y", "${MCP_PACKAGE_SPEC}", "--server"]`;
135
257
  const warmBlock = `[mcp_servers.warm]\ncommand = "${tomlCommand}"\nargs = ${tomlArgs}\n`;
136
- const warmBlockPattern = /\n?\[mcp_servers\.warm\][\s\S]*?(?=\n\[[^\n]+\]|\s*$)/g;
137
- let nextContent = content.replace(warmBlockPattern, '').trimEnd();
258
+ const preservedEnvLines = getTomlEnvStatus(content).envLines.filter((line) => !/^\s*WARM_API_KEY\s*=/.test(line));
259
+ let nextContent = content
260
+ .replace(WARM_TOML_ENV_BLOCK_PATTERN, '')
261
+ .replace(WARM_TOML_BLOCK_PATTERN, '')
262
+ .trimEnd();
138
263
  if (nextContent.length > 0) {
139
264
  nextContent += '\n\n';
140
265
  }
141
266
  nextContent += warmBlock;
267
+ if (preservedEnvLines.length > 0) {
268
+ nextContent += `\n[mcp_servers.warm.env]\n${preservedEnvLines.join('\n')}\n`;
269
+ }
142
270
  mkdirSync(dirname(client.configPath), { recursive: true });
143
271
  writeFileSync(client.configPath, nextContent.endsWith('\n') ? nextContent : `${nextContent}\n`);
144
272
  }
@@ -197,6 +325,18 @@ function storeApiKey(apiKey) {
197
325
  mkdirSync(dirname(WARM_API_KEY_PATH), { recursive: true });
198
326
  writeFileSync(WARM_API_KEY_PATH, `${apiKey}\n`, { mode: 0o600 });
199
327
  }
328
+ function getStatusLabel(status, force) {
329
+ if (force) {
330
+ return 'not configured';
331
+ }
332
+ if (status === 'configured') {
333
+ return 'configured';
334
+ }
335
+ if (status === 'needs_migration') {
336
+ return 'needs migration';
337
+ }
338
+ return 'not configured';
339
+ }
200
340
  export async function install(options = {}) {
201
341
  const force = options.force ?? false;
202
342
  const shouldValidateApiKey = options.validateApiKey ?? true;
@@ -207,12 +347,11 @@ export async function install(options = {}) {
207
347
  const globalClients = GLOBAL_CLIENTS.filter(isDetected);
208
348
  const projectClients = detectProjectClients();
209
349
  const allClients = [...globalClients, ...projectClients];
210
- const needsSetup = allClients.filter((client) => !isConfigured(client) || force);
350
+ const clientStatuses = allClients.map(getClientStatus);
351
+ const needsSetup = clientStatuses.filter((status) => force || status.state !== 'configured');
211
352
  console.log(' MCP clients found:');
212
- allClients.forEach((client) => {
213
- const configured = isConfigured(client);
214
- const status = configured && !force ? 'configured' : 'not configured';
215
- console.log(` ${client.name.padEnd(22)} ${shortPath(client.configPath).padEnd(55)} ${status}`);
353
+ clientStatuses.forEach((status) => {
354
+ console.log(` ${status.client.name.padEnd(22)} ${shortPath(status.client.configPath).padEnd(55)} ${getStatusLabel(status.state, force)}`);
216
355
  });
217
356
  console.log('');
218
357
  if (needsSetup.length === 0) {
@@ -221,34 +360,69 @@ export async function install(options = {}) {
221
360
  console.log('');
222
361
  return;
223
362
  }
224
- const apiKey = await prompt(' Warm API key: ');
225
- if (!apiKey) {
226
- console.log('');
227
- console.log(' No key provided. Get one at https://warm.io/settings');
228
- console.log('');
229
- return;
363
+ const storedApiKey = readConfigFile(WARM_API_KEY_PATH);
364
+ const migratedApiKey = needsSetup.find((status) => typeof status.inlineApiKey === 'string' && status.inlineApiKey.length > 0)
365
+ ?.inlineApiKey ?? null;
366
+ const hasApiKeyFileOverride = needsSetup.some((status) => status.hasApiKeyFileOverride);
367
+ let apiKeyToStore = null;
368
+ let shouldStoreApiKey = false;
369
+ let shouldPromptForApiKey = force;
370
+ if (!shouldPromptForApiKey) {
371
+ if (storedApiKey) {
372
+ console.log(' Using stored Warm API key.');
373
+ console.log('');
374
+ apiKeyToStore = storedApiKey;
375
+ }
376
+ else if (migratedApiKey) {
377
+ console.log(' Reusing Warm API key from existing client config.');
378
+ console.log('');
379
+ apiKeyToStore = migratedApiKey;
380
+ shouldStoreApiKey = true;
381
+ }
382
+ else if (hasApiKeyFileOverride) {
383
+ console.log(' Reusing existing WARM_API_KEY_FILE override.');
384
+ console.log('');
385
+ }
386
+ else {
387
+ shouldPromptForApiKey = true;
388
+ }
389
+ }
390
+ if (shouldPromptForApiKey) {
391
+ const apiKey = await prompt(' Warm API key: ');
392
+ if (!apiKey) {
393
+ console.log('');
394
+ console.log(' No key provided. Get one at https://warm.io/settings');
395
+ console.log('');
396
+ return;
397
+ }
398
+ apiKeyToStore = apiKey;
399
+ shouldStoreApiKey = true;
230
400
  }
231
- if (shouldValidateApiKey) {
232
- const isValid = await validateApiKey(apiKey);
401
+ if (apiKeyToStore && shouldValidateApiKey && (shouldStoreApiKey || force)) {
402
+ const isValid = await validateApiKey(apiKeyToStore);
233
403
  if (!isValid) {
234
404
  return;
235
405
  }
236
406
  }
237
- storeApiKey(apiKey);
407
+ if (apiKeyToStore && shouldStoreApiKey) {
408
+ storeApiKey(apiKeyToStore);
409
+ }
238
410
  console.log(' Configuring...');
239
411
  console.log('');
240
- needsSetup.forEach((client) => {
412
+ needsSetup.forEach((status) => {
241
413
  try {
242
- configure(client);
243
- console.log(` ${client.name.padEnd(22)} done`);
414
+ configure(status.client);
415
+ console.log(` ${status.client.name.padEnd(22)} done`);
244
416
  }
245
417
  catch (error) {
246
418
  const message = error instanceof Error ? error.message : String(error);
247
- console.log(` ${client.name.padEnd(22)} failed: ${message}`);
419
+ console.log(` ${status.client.name.padEnd(22)} failed: ${message}`);
248
420
  }
249
421
  });
250
422
  console.log('');
251
- console.log(` Stored API key at ${shortPath(WARM_API_KEY_PATH)}`);
423
+ if (apiKeyToStore) {
424
+ console.log(` Stored API key at ${shortPath(WARM_API_KEY_PATH)}`);
425
+ }
252
426
  console.log(' All set! Restart your MCP clients and try:');
253
427
  console.log(' "What\'s my net worth?"');
254
428
  console.log('');
@@ -0,0 +1 @@
1
+ export declare function loadDotEnv(): void;
@@ -0,0 +1,36 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { config as loadEnvFile } from 'dotenv';
5
+ let loaded = false;
6
+ export function loadDotEnv() {
7
+ if (loaded) {
8
+ return;
9
+ }
10
+ loaded = true;
11
+ const candidates = new Set();
12
+ let currentDir = process.cwd();
13
+ while (true) {
14
+ candidates.add(path.join(currentDir, '.env'));
15
+ const parentDir = path.dirname(currentDir);
16
+ if (parentDir === currentDir) {
17
+ break;
18
+ }
19
+ currentDir = parentDir;
20
+ }
21
+ let moduleDir = path.dirname(fileURLToPath(import.meta.url));
22
+ while (true) {
23
+ candidates.add(path.join(moduleDir, '.env'));
24
+ const parentDir = path.dirname(moduleDir);
25
+ if (parentDir === moduleDir) {
26
+ break;
27
+ }
28
+ moduleDir = parentDir;
29
+ }
30
+ for (const candidate of candidates) {
31
+ if (fs.existsSync(candidate)) {
32
+ loadEnvFile({ path: candidate, override: false, quiet: true });
33
+ return;
34
+ }
35
+ }
36
+ }