@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 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
- existing.env = { WARM_API_KEY: apiKey };
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
- content += `\n[mcp_servers.warm]\ncommand = "${tomlCommand}"\nargs = ${tomlArgs}\n\n[mcp_servers.warm.env]\nWARM_API_KEY = "${apiKey}"\n`;
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, content);
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
- return process.env.WARM_API_KEY;
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
- return fs.readFileSync(configPath, 'utf-8').trim();
65
+ cachedApiKey = fs.readFileSync(configPath, 'utf-8').trim();
54
66
  }
55
67
  catch {
56
- return null;
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
- const response = await fetch(url.toString(), {
70
- headers: {
71
- Authorization: `Bearer ${apiKey}`,
72
- Accept: 'application/json',
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.0' }, { capabilities: { tools: {} } });
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 requestedLimit = args?.limit ? Math.min(Number(args.limit), 200) : 50;
179
- // Fetch more if filtering, since we'll reduce the set
180
- const fetchLimit = search || until ? Math.min(requestedLimit * 10, 1000) : requestedLimit;
181
- const params = { limit: String(fetchLimit) };
182
- if (since)
183
- params.last_knowledge = since;
184
- const response = (await apiRequest('/api/transactions', params));
185
- let transactions = (response.transactions || []);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warmio/mcp",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "MCP server for Warm Financial API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",