mcp-compress-router 1.6.2 → 1.6.3

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.
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
- import { parseJsonc } from '../utils/index.js';
3
+ import { atomicWriteFile, parseJsonc } from '../utils/index.js';
4
4
  /**
5
5
  * Type guard for Node.js system errors that carry a `code` property.
6
6
  *
@@ -32,7 +32,7 @@ export async function ensureConfigDir(configPath) {
32
32
  await fs.access(configPath);
33
33
  }
34
34
  catch {
35
- await fs.writeFile(configPath, JSON.stringify({ mcpServers: {} }, null, 2) + '\n');
35
+ await atomicWriteFile(configPath, JSON.stringify({ mcpServers: {} }, null, 2) + '\n');
36
36
  }
37
37
  }
38
38
  /**
@@ -86,7 +86,7 @@ export async function writeConfigFile(configPath, mcpServers) {
86
86
  // credentials.json separation
87
87
  delete existing.credentials;
88
88
  existing.mcpServers = mcpServers;
89
- await fs.writeFile(configPath, JSON.stringify(existing, null, 2) + '\n');
89
+ await atomicWriteFile(configPath, JSON.stringify(existing, null, 2) + '\n');
90
90
  }
91
91
  /**
92
92
  * Reads the credentials object from credentials.json.
@@ -136,7 +136,7 @@ export async function writeCredentials(configPath, name, credentials, logger) {
136
136
  // Write atomically (unique temporary sibling + rename): a crash or
137
137
  // forced exit can never leave credentials.json truncated, and
138
138
  // readCredentials treats invalid JSON as a hard startup error.
139
- await writeCredentialsFileAtomic(credPath, JSON.stringify(store, null, 2) + '\n', mode);
139
+ await atomicWriteFile(credPath, JSON.stringify(store, null, 2) + '\n', mode);
140
140
  // On first creation, set restrictive permissions (owner read/write only)
141
141
  if (isNewFile) {
142
142
  await restrictNewCredentialsFile(credPath, logger);
@@ -183,26 +183,6 @@ async function resolveCredentialsMode(credPath) {
183
183
  return { isNewFile: true, mode: 0o600 };
184
184
  }
185
185
  }
186
- /**
187
- * Writes `data` atomically: the content goes to a unique temporary
188
- * sibling file first, then is renamed over the target, so a crash or a
189
- * forced exit can never leave a truncated credentials file behind.
190
- *
191
- * @param credPath - Target credentials file path.
192
- * @param data - Full file content to write.
193
- * @param mode - Permissions to create the replacement file with.
194
- */
195
- async function writeCredentialsFileAtomic(credPath, data, mode) {
196
- const tempPath = `${credPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
197
- try {
198
- await fs.writeFile(tempPath, data, { mode });
199
- await fs.rename(tempPath, credPath);
200
- }
201
- catch (err) {
202
- await fs.unlink(tempPath).catch(() => { });
203
- throw err;
204
- }
205
- }
206
186
  /**
207
187
  * Restricts a freshly created credentials file to owner read/write and
208
188
  * warns when the platform cannot enforce it.
@@ -248,6 +228,6 @@ export async function removeCredentials(configPath, name) {
248
228
  }
249
229
  else {
250
230
  const { mode } = await resolveCredentialsMode(credPath);
251
- await writeCredentialsFileAtomic(credPath, JSON.stringify(store, null, 2) + '\n', mode);
231
+ await atomicWriteFile(credPath, JSON.stringify(store, null, 2) + '\n', mode);
252
232
  }
253
233
  }
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
+ import { atomicWriteFile } from '../utils/index.js';
3
4
  /**
4
5
  * Per-cache-file write queues. Each value is the promise of the most
5
6
  * recently queued write for that file. New writes chain onto it so all
@@ -102,28 +103,6 @@ async function readCacheStore(configPath) {
102
103
  return {};
103
104
  }
104
105
  }
105
- /**
106
- * Writes `data` to `cachePath` atomically: the content is first written
107
- * to a unique temporary sibling file and then renamed over the target.
108
- * Renaming within the same directory is atomic on POSIX, so a reader
109
- * (or a crash mid-write) can never observe a truncated or half-written
110
- * cache file — it sees either the previous complete file or the new
111
- * complete file.
112
- *
113
- * @param cachePath - Target cache file path.
114
- * @param data - Full file content to write.
115
- */
116
- async function writeCacheFileAtomic(cachePath, data) {
117
- const tempPath = `${cachePath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
118
- try {
119
- await fs.writeFile(tempPath, data);
120
- await fs.rename(tempPath, cachePath);
121
- }
122
- catch (err) {
123
- await fs.unlink(tempPath).catch(() => { });
124
- throw err;
125
- }
126
- }
127
106
  /**
128
107
  * Saves discovered tools to the on-disk tool cache for a single server.
129
108
  * Preserves other servers' cached entries. Overwrites the entry for the
@@ -143,7 +122,7 @@ export async function saveToolCache(configPath, serverName, tools) {
143
122
  tools,
144
123
  cachedAt: new Date().toISOString(),
145
124
  };
146
- await writeCacheFileAtomic(cachePath, JSON.stringify(store, null, 2) + '\n');
125
+ await atomicWriteFile(cachePath, JSON.stringify(store, null, 2) + '\n');
147
126
  });
148
127
  }
149
128
  /**
@@ -188,7 +167,7 @@ export async function clearToolCache(configPath, serverName) {
188
167
  await fs.unlink(cachePath).catch(() => { });
189
168
  }
190
169
  else {
191
- await writeCacheFileAtomic(cachePath, JSON.stringify(store, null, 2) + '\n');
170
+ await atomicWriteFile(cachePath, JSON.stringify(store, null, 2) + '\n');
192
171
  }
193
172
  });
194
173
  }
@@ -0,0 +1,31 @@
1
+ import * as fs from 'node:fs/promises';
2
+ /**
3
+ * Writes a file atomically: the content is first written to a unique
4
+ * temporary sibling file, then renamed over the target. Renaming within
5
+ * the same directory is atomic on POSIX, so a reader or a concurrent
6
+ * writer can only ever observe the old complete file or the new complete
7
+ * file — never a torn or empty one. The temporary file is removed when
8
+ * the write or the rename fails.
9
+ *
10
+ * The optional `mode` is applied to the temporary file (the rename
11
+ * preserves it), so callers can keep a sensitive file's permissions —
12
+ * e.g. `credentials.json` must stay owner-only across rewrites.
13
+ *
14
+ * @param filePath - Absolute path to the destination file.
15
+ * @param content - The full content to write.
16
+ * @param mode - Optional file mode (e.g. `0o600`); omitted to use the
17
+ * default umask-based mode.
18
+ * @throws If writing the temporary file or renaming it over the target
19
+ * fails. The temporary file is removed before the error propagates.
20
+ */
21
+ export async function atomicWriteFile(filePath, content, mode) {
22
+ const tempPath = `${filePath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
23
+ try {
24
+ await fs.writeFile(tempPath, content, mode !== undefined ? { mode } : undefined);
25
+ await fs.rename(tempPath, filePath);
26
+ }
27
+ catch (err) {
28
+ await fs.unlink(tempPath).catch(() => { });
29
+ throw err;
30
+ }
31
+ }
@@ -5,5 +5,6 @@ export { validateGlobPattern } from './validate-glob.js';
5
5
  export { expandEnvField } from './expand-env.js';
6
6
  export { Logger } from './logger.js';
7
7
  export { parseJsonc } from './parse-jsonc.js';
8
+ export { atomicWriteFile } from './atomic-write.js';
8
9
  export { VALID_COMPRESSION_LEVELS, isCompressionLevel } from './compression-level.js';
9
10
  export { getDownstreamTimeoutMs, getAuthDiscoveryTimeoutMs, createTimeoutFetch, } from './timeout.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-compress-router",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "Compress all connected MCP servers into a single router MCP to save tokens",
5
5
  "license": "MIT",
6
6
  "type": "module",