@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 +26 -4
- package/dist/config-paths.d.ts +1 -0
- package/dist/config-paths.js +9 -0
- package/dist/http.d.ts +5 -6
- package/dist/http.js +12 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +47 -3
- package/dist/install.js +209 -35
- package/dist/load-env.d.ts +1 -0
- package/dist/load-env.js +36 -0
- package/dist/schemas.d.ts +242 -191
- package/dist/schemas.js +140 -163
- package/dist/server.d.ts +15 -8
- package/dist/server.js +105 -12
- package/dist/stdio.d.ts +29 -0
- package/dist/stdio.js +200 -0
- package/dist/types.d.ts +2 -158
- package/dist/warm-api-client.d.ts +184 -0
- package/dist/warm-api-client.js +299 -0
- package/dist/warm-server.d.ts +2 -14
- package/dist/warm-server.js +2 -245
- package/package.json +15 -6
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
|
|
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:
|
package/dist/config-paths.d.ts
CHANGED
package/dist/config-paths.js
CHANGED
|
@@ -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
|
-
|
|
2
|
-
|
|
1
|
+
export interface HttpServerOptions {
|
|
2
|
+
allowedHosts?: string[];
|
|
3
3
|
host?: string;
|
|
4
|
-
port?: number;
|
|
5
4
|
path?: string;
|
|
6
|
-
|
|
5
|
+
port?: number;
|
|
7
6
|
}
|
|
8
|
-
export declare function resolveHttpServerOptions(overrides?:
|
|
9
|
-
export declare function startHttpServer(options?:
|
|
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
|
-
|
|
90
|
+
sendJsonMethodNotAllowed(res);
|
|
89
91
|
});
|
|
90
92
|
app.delete(resolved.path, (_req, res) => {
|
|
91
|
-
|
|
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 {
|
|
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',
|
|
51
|
-
: { command: 'npx', args: ['-y',
|
|
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
|
|
75
|
-
if (!
|
|
76
|
-
return
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
?
|
|
134
|
-
:
|
|
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
|
|
137
|
-
let nextContent = content
|
|
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
|
|
350
|
+
const clientStatuses = allClients.map(getClientStatus);
|
|
351
|
+
const needsSetup = clientStatuses.filter((status) => force || status.state !== 'configured');
|
|
211
352
|
console.log(' MCP clients found:');
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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(
|
|
401
|
+
if (apiKeyToStore && shouldValidateApiKey && (shouldStoreApiKey || force)) {
|
|
402
|
+
const isValid = await validateApiKey(apiKeyToStore);
|
|
233
403
|
if (!isValid) {
|
|
234
404
|
return;
|
|
235
405
|
}
|
|
236
406
|
}
|
|
237
|
-
|
|
407
|
+
if (apiKeyToStore && shouldStoreApiKey) {
|
|
408
|
+
storeApiKey(apiKeyToStore);
|
|
409
|
+
}
|
|
238
410
|
console.log(' Configuring...');
|
|
239
411
|
console.log('');
|
|
240
|
-
needsSetup.forEach((
|
|
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
|
-
|
|
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;
|
package/dist/load-env.js
ADDED
|
@@ -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
|
+
}
|