@warmio/mcp 4.3.1 → 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 +11 -4
- package/dist/index.js +0 -0
- package/dist/install.js +209 -35
- package/dist/load-env.js +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +66 -34
- package/dist/warm-api-client.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,13 +7,17 @@ This package starts in local `stdio` mode by default and also supports optional
|
|
|
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,7 +59,7 @@ 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
|
}
|
|
@@ -63,7 +70,7 @@ On Windows, prefer:
|
|
|
63
70
|
If your MCP client supports Streamable HTTP, you can run:
|
|
64
71
|
|
|
65
72
|
```bash
|
|
66
|
-
npx @warmio/mcp http --host 127.0.0.1 --port 3000 --path /mcp
|
|
73
|
+
npx -y @warmio/mcp@latest http --host 127.0.0.1 --port 3000 --path /mcp
|
|
67
74
|
```
|
|
68
75
|
|
|
69
76
|
Relevant optional environment variables:
|
package/dist/index.js
CHANGED
|
File without changes
|
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('');
|
package/dist/load-env.js
CHANGED
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type Implementation } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { createWarmApiClient, type WarmApiClientOptions, getConfiguredApiKey, verifyWarmApiKey, apiRequest } from './warm-api-client.js';
|
|
4
4
|
export { apiRequest, createWarmApiClient, getConfiguredApiKey, verifyWarmApiKey };
|
|
5
5
|
export type { WarmApiClientOptions } from './warm-api-client.js';
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { toJsonSchemaCompat } from '@modelcontextprotocol/sdk/server/zod-json-schema-compat.js';
|
|
4
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { z } from 'zod';
|
|
3
6
|
import { createWarmApiClient, getConfiguredApiKey, verifyWarmApiKey, apiRequest, } from './warm-api-client.js';
|
|
4
|
-
import { emptyInputSchema,
|
|
7
|
+
import { emptyInputSchema, getTransactionsInputSchema, } from './schemas.js';
|
|
5
8
|
export { apiRequest, createWarmApiClient, getConfiguredApiKey, verifyWarmApiKey };
|
|
6
9
|
export const API_URL = process.env.WARM_API_URL || 'https://warm.io';
|
|
7
10
|
function getPackageVersion() {
|
|
@@ -18,12 +21,6 @@ export const WARM_SERVER_INFO = {
|
|
|
18
21
|
name: 'warm',
|
|
19
22
|
version: getPackageVersion(),
|
|
20
23
|
};
|
|
21
|
-
const READ_ONLY_TOOL_ANNOTATIONS = {
|
|
22
|
-
destructiveHint: false,
|
|
23
|
-
idempotentHint: true,
|
|
24
|
-
openWorldHint: false,
|
|
25
|
-
readOnlyHint: true,
|
|
26
|
-
};
|
|
27
24
|
function createStructuredToolResult(structuredContent) {
|
|
28
25
|
return {
|
|
29
26
|
content: [
|
|
@@ -37,35 +34,70 @@ function createStructuredToolResult(structuredContent) {
|
|
|
37
34
|
}
|
|
38
35
|
export function registerWarmTools(server, options = {}) {
|
|
39
36
|
const client = createWarmApiClient(options);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
37
|
+
const tools = [
|
|
38
|
+
{
|
|
39
|
+
description: 'Read-only list of connected financial accounts with balances, subtypes, institutions, and masks when available.',
|
|
40
|
+
handler: async () => await client.getAccounts(),
|
|
41
|
+
inputShape: emptyInputSchema,
|
|
42
|
+
name: 'get_accounts',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
description: 'Read-only transaction export with strict opaque cursor pagination and optional last_knowledge incremental sync.',
|
|
46
|
+
handler: async (args) => {
|
|
47
|
+
const transactionArgs = {
|
|
48
|
+
cursor: typeof args.cursor === 'string' ? args.cursor : undefined,
|
|
49
|
+
last_knowledge: typeof args.last_knowledge === 'string' ? args.last_knowledge : undefined,
|
|
50
|
+
limit: typeof args.limit === 'number' ? args.limit : 500,
|
|
51
|
+
search: typeof args.search === 'string' ? args.search : undefined,
|
|
52
|
+
};
|
|
53
|
+
if (transactionArgs.cursor && transactionArgs.last_knowledge) {
|
|
54
|
+
throw new Error('`cursor` cannot be combined with `last_knowledge`.');
|
|
55
|
+
}
|
|
56
|
+
return await client.getTransactions(transactionArgs);
|
|
57
|
+
},
|
|
58
|
+
inputShape: getTransactionsInputSchema,
|
|
59
|
+
name: 'get_transactions',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
description: 'Read-only broad financial state bundle with snapshots, recurring payments, budgets, goals, financial health, liabilities, holdings, and category spending.',
|
|
63
|
+
handler: async () => await client.getFinancialState(),
|
|
64
|
+
inputShape: emptyInputSchema,
|
|
65
|
+
name: 'get_financial_state',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
description: 'Read-only API key validation for the configured Warm account.',
|
|
69
|
+
handler: async () => await client.verifyKey(),
|
|
70
|
+
inputShape: emptyInputSchema,
|
|
71
|
+
name: 'verify_key',
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
server.server.registerCapabilities({
|
|
75
|
+
tools: {
|
|
76
|
+
listChanged: true,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
server.server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
80
|
+
tools: tools.map((tool) => ({
|
|
81
|
+
description: tool.description,
|
|
82
|
+
inputSchema: toJsonSchemaCompat(z.object(tool.inputShape).strict(), {
|
|
83
|
+
pipeStrategy: 'input',
|
|
84
|
+
strictUnions: true,
|
|
85
|
+
}),
|
|
86
|
+
name: tool.name,
|
|
87
|
+
})),
|
|
88
|
+
}));
|
|
89
|
+
server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
90
|
+
const tool = tools.find((candidate) => candidate.name === request.params.name);
|
|
91
|
+
if (!tool) {
|
|
92
|
+
throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
|
|
93
|
+
}
|
|
94
|
+
const argsSchema = z.object(tool.inputShape).strict();
|
|
95
|
+
const parseResult = await argsSchema.safeParseAsync(request.params.arguments ?? {});
|
|
96
|
+
if (!parseResult.success) {
|
|
97
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for tool ${request.params.name}: ${parseResult.error.message}`);
|
|
54
98
|
}
|
|
55
|
-
return createStructuredToolResult(await
|
|
99
|
+
return createStructuredToolResult(await tool.handler(parseResult.data));
|
|
56
100
|
});
|
|
57
|
-
server.registerTool('get_financial_state', {
|
|
58
|
-
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
59
|
-
description: 'Read-only broad financial state bundle with snapshots, recurring payments, budgets, goals, financial health, liabilities, holdings, and category spending.',
|
|
60
|
-
inputSchema: emptyInputSchema,
|
|
61
|
-
outputSchema: getFinancialStateOutputSchema,
|
|
62
|
-
}, async () => createStructuredToolResult(await client.getFinancialState()));
|
|
63
|
-
server.registerTool('verify_key', {
|
|
64
|
-
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
65
|
-
description: 'Read-only API key validation for the configured Warm account.',
|
|
66
|
-
inputSchema: emptyInputSchema,
|
|
67
|
-
outputSchema: verifyKeyOutputSchema,
|
|
68
|
-
}, async () => createStructuredToolResult(await client.verifyKey()));
|
|
69
101
|
}
|
|
70
102
|
export function createWarmServer(options = {}) {
|
|
71
103
|
const { serverInfo, ...clientOptions } = options;
|
package/dist/warm-api-client.js
CHANGED
|
@@ -163,7 +163,7 @@ export async function apiRequest(endpoint, params = {}, options = {}) {
|
|
|
163
163
|
const requestOptions = createWarmApiClientConfig(options);
|
|
164
164
|
const apiKey = requestOptions.apiKeyResolver?.() ?? getConfiguredApiKey();
|
|
165
165
|
if (!apiKey) {
|
|
166
|
-
throw new Error('WARM_API_KEY not set. Run "npx @warmio/mcp" to configure or set WARM_API_KEY.');
|
|
166
|
+
throw new Error('WARM_API_KEY not set. Run "npx -y @warmio/mcp@latest" to configure or set WARM_API_KEY.');
|
|
167
167
|
}
|
|
168
168
|
const url = new URL(endpoint, requestOptions.apiUrl);
|
|
169
169
|
Object.entries(params).forEach(([key, value]) => {
|