@warmio/mcp 1.2.0 → 1.2.1
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/dist/install.js +15 -6
- package/dist/server.js +72 -19
- package/package.json +1 -1
package/dist/install.js
CHANGED
|
@@ -4,6 +4,9 @@ import { homedir, platform } from 'os';
|
|
|
4
4
|
import { createInterface } from 'readline';
|
|
5
5
|
const HOME = homedir();
|
|
6
6
|
const CWD = process.cwd();
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
7
10
|
function getClaudeDesktopPath() {
|
|
8
11
|
if (platform() === 'win32') {
|
|
9
12
|
return join(process.env.APPDATA || join(HOME, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
|
|
@@ -74,14 +77,14 @@ function configureJson(client, apiKey) {
|
|
|
74
77
|
if (!config.mcpServers)
|
|
75
78
|
config.mcpServers = {};
|
|
76
79
|
const servers = config.mcpServers;
|
|
77
|
-
const existing = servers.warm;
|
|
80
|
+
const existing = isRecord(servers.warm) ? servers.warm : undefined;
|
|
81
|
+
const existingEnv = isRecord(existing?.env) ? existing.env : {};
|
|
78
82
|
// For project-level configs, preserve existing command/args if present — only inject the key.
|
|
79
83
|
if (client.isProjectLevel && existing?.command) {
|
|
80
|
-
|
|
81
|
-
servers.warm = existing;
|
|
84
|
+
servers.warm = { ...existing, env: { ...existingEnv, WARM_API_KEY: apiKey } };
|
|
82
85
|
}
|
|
83
86
|
else {
|
|
84
|
-
servers.warm = { ...MCP_CONFIG, env: { WARM_API_KEY: apiKey } };
|
|
87
|
+
servers.warm = { ...existing, ...MCP_CONFIG, env: { ...existingEnv, WARM_API_KEY: apiKey } };
|
|
85
88
|
}
|
|
86
89
|
mkdirSync(dirname(client.configPath), { recursive: true });
|
|
87
90
|
writeFileSync(client.configPath, JSON.stringify(config, null, 2) + '\n');
|
|
@@ -97,9 +100,15 @@ function configureToml(client, apiKey) {
|
|
|
97
100
|
const tomlArgs = platform() === 'win32'
|
|
98
101
|
? '["/c", "npx", "-y", "@warmio/mcp", "--server"]'
|
|
99
102
|
: '["-y", "@warmio/mcp", "--server"]';
|
|
100
|
-
|
|
103
|
+
const warmBlock = `[mcp_servers.warm]\ncommand = "${tomlCommand}"\nargs = ${tomlArgs}\n\n[mcp_servers.warm.env]\nWARM_API_KEY = "${apiKey}"\n`;
|
|
104
|
+
const warmBlockPattern = /\n?\[mcp_servers\.warm\][\s\S]*?(?=\n\[[^\n]+\]|\s*$)/g;
|
|
105
|
+
let nextContent = content.replace(warmBlockPattern, '').trimEnd();
|
|
106
|
+
if (nextContent.length > 0) {
|
|
107
|
+
nextContent += '\n\n';
|
|
108
|
+
}
|
|
109
|
+
nextContent += warmBlock;
|
|
101
110
|
mkdirSync(dirname(client.configPath), { recursive: true });
|
|
102
|
-
writeFileSync(client.configPath,
|
|
111
|
+
writeFileSync(client.configPath, nextContent.endsWith('\n') ? nextContent : `${nextContent}\n`);
|
|
103
112
|
}
|
|
104
113
|
function configure(client, apiKey) {
|
|
105
114
|
if (client.format === 'json')
|
package/dist/server.js
CHANGED
|
@@ -12,6 +12,14 @@ import * as path from 'path';
|
|
|
12
12
|
import * as os from 'os';
|
|
13
13
|
const API_URL = process.env.WARM_API_URL || 'https://warm.io';
|
|
14
14
|
const MAX_RESPONSE_SIZE = 50_000;
|
|
15
|
+
const MAX_TRANSACTION_PAGES = 10;
|
|
16
|
+
const MAX_TRANSACTION_SCAN = 5_000;
|
|
17
|
+
const TRANSACTION_PAGE_SIZE = 200;
|
|
18
|
+
const REQUEST_TIMEOUT_MS = (() => {
|
|
19
|
+
const raw = Number(process.env.WARM_API_TIMEOUT_MS || 10_000);
|
|
20
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 10_000;
|
|
21
|
+
})();
|
|
22
|
+
let cachedApiKey;
|
|
15
23
|
function compactTransaction(t) {
|
|
16
24
|
return {
|
|
17
25
|
d: t.date,
|
|
@@ -45,16 +53,30 @@ function calculateSummary(transactions) {
|
|
|
45
53
|
};
|
|
46
54
|
}
|
|
47
55
|
function getApiKey() {
|
|
56
|
+
if (cachedApiKey !== undefined) {
|
|
57
|
+
return cachedApiKey;
|
|
58
|
+
}
|
|
48
59
|
if (process.env.WARM_API_KEY) {
|
|
49
|
-
|
|
60
|
+
cachedApiKey = process.env.WARM_API_KEY;
|
|
61
|
+
return cachedApiKey;
|
|
50
62
|
}
|
|
51
63
|
const configPath = path.join(os.homedir(), '.config', 'warm', 'api_key');
|
|
52
64
|
try {
|
|
53
|
-
|
|
65
|
+
cachedApiKey = fs.readFileSync(configPath, 'utf-8').trim();
|
|
54
66
|
}
|
|
55
67
|
catch {
|
|
56
|
-
|
|
68
|
+
cachedApiKey = null;
|
|
69
|
+
}
|
|
70
|
+
return cachedApiKey;
|
|
71
|
+
}
|
|
72
|
+
function getRequestSignal(timeoutMs) {
|
|
73
|
+
if (typeof AbortSignal.timeout === 'function') {
|
|
74
|
+
return AbortSignal.timeout(timeoutMs);
|
|
57
75
|
}
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
78
|
+
timer.unref?.();
|
|
79
|
+
return controller.signal;
|
|
58
80
|
}
|
|
59
81
|
async function apiRequest(endpoint, params = {}) {
|
|
60
82
|
const apiKey = getApiKey();
|
|
@@ -66,12 +88,25 @@ async function apiRequest(endpoint, params = {}) {
|
|
|
66
88
|
if (value)
|
|
67
89
|
url.searchParams.append(key, value);
|
|
68
90
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
let response;
|
|
92
|
+
try {
|
|
93
|
+
response = await fetch(url.toString(), {
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: `Bearer ${apiKey}`,
|
|
96
|
+
Accept: 'application/json',
|
|
97
|
+
},
|
|
98
|
+
signal: getRequestSignal(REQUEST_TIMEOUT_MS),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error instanceof Error && error.name === 'TimeoutError') {
|
|
103
|
+
throw new Error(`Warm API timed out after ${REQUEST_TIMEOUT_MS}ms`);
|
|
104
|
+
}
|
|
105
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
106
|
+
throw new Error(`Warm API request aborted after ${REQUEST_TIMEOUT_MS}ms`);
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
75
110
|
if (!response.ok) {
|
|
76
111
|
const errorMessages = {
|
|
77
112
|
401: 'Invalid or expired API key. Regenerate at https://warm.io/settings',
|
|
@@ -82,7 +117,7 @@ async function apiRequest(endpoint, params = {}) {
|
|
|
82
117
|
}
|
|
83
118
|
return response.json();
|
|
84
119
|
}
|
|
85
|
-
const server = new Server({ name: 'warm', version: '1.2.
|
|
120
|
+
const server = new Server({ name: 'warm', version: '1.2.1' }, { capabilities: { tools: {} } });
|
|
86
121
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
87
122
|
tools: [
|
|
88
123
|
{
|
|
@@ -175,14 +210,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
175
210
|
const search = args?.search ? String(args.search) : undefined;
|
|
176
211
|
const since = args?.since ? String(args.since) : undefined;
|
|
177
212
|
const until = args?.until ? String(args.until) : undefined;
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
let
|
|
213
|
+
const parsedLimit = args?.limit ? Number(args.limit) : 50;
|
|
214
|
+
const requestedLimit = Number.isFinite(parsedLimit)
|
|
215
|
+
? Math.max(1, Math.min(Math.floor(parsedLimit), 200))
|
|
216
|
+
: 50;
|
|
217
|
+
const needsClientFiltering = Boolean(search || until);
|
|
218
|
+
let transactions = [];
|
|
219
|
+
let cursor;
|
|
220
|
+
let pagesFetched = 0;
|
|
221
|
+
let scanned = 0;
|
|
222
|
+
do {
|
|
223
|
+
const params = {
|
|
224
|
+
limit: String(needsClientFiltering ? TRANSACTION_PAGE_SIZE : requestedLimit),
|
|
225
|
+
};
|
|
226
|
+
if (since)
|
|
227
|
+
params.last_knowledge = since;
|
|
228
|
+
if (cursor)
|
|
229
|
+
params.cursor = cursor;
|
|
230
|
+
const response = (await apiRequest('/api/transactions', params));
|
|
231
|
+
const batch = (response.transactions || []);
|
|
232
|
+
transactions.push(...batch);
|
|
233
|
+
scanned += batch.length;
|
|
234
|
+
pagesFetched += 1;
|
|
235
|
+
cursor = response.cursor;
|
|
236
|
+
if (!needsClientFiltering) {
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
} while (cursor && pagesFetched < MAX_TRANSACTION_PAGES && scanned < MAX_TRANSACTION_SCAN);
|
|
186
240
|
// Apply date range filter (until is client-side since API only supports since)
|
|
187
241
|
if (until) {
|
|
188
242
|
transactions = transactions.filter((t) => inDateRange(t, since, until));
|
|
@@ -191,7 +245,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
191
245
|
if (search) {
|
|
192
246
|
transactions = transactions.filter((t) => matchesSearch(t, search));
|
|
193
247
|
}
|
|
194
|
-
// Convert to compact format
|
|
195
248
|
const compactTxns = transactions.map(compactTransaction);
|
|
196
249
|
// Calculate summary on ALL matching transactions
|
|
197
250
|
const summary = calculateSummary(compactTxns);
|