mcp-compress-router 1.6.0 → 1.6.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/build/services/tool-cache.js +59 -14
- package/package.json +1 -1
|
@@ -47,13 +47,39 @@ function serializeCacheWrite(cachePath, task) {
|
|
|
47
47
|
function isNodeError(err) {
|
|
48
48
|
return err instanceof Error && typeof err.code === 'string';
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Sanitizes a parsed cache store: keeps only entries that are plain
|
|
52
|
+
* objects carrying a `tools` array, and drops everything else. A cache
|
|
53
|
+
* file that is damaged in parts therefore degrades to the subset of
|
|
54
|
+
* still-valid entries instead of being rejected wholesale; a root that
|
|
55
|
+
* is not a plain object degrades to an empty store.
|
|
56
|
+
*
|
|
57
|
+
* @param parsed - The raw JSON.parse() result of the cache file.
|
|
58
|
+
* @returns A validated store containing only well-formed entries.
|
|
59
|
+
*/
|
|
60
|
+
function sanitizeStore(parsed) {
|
|
61
|
+
const store = {};
|
|
62
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
63
|
+
return store;
|
|
64
|
+
}
|
|
65
|
+
for (const [serverName, entry] of Object.entries(parsed)) {
|
|
66
|
+
const candidate = entry;
|
|
67
|
+
if (candidate !== null && typeof candidate === 'object' && Array.isArray(candidate.tools)) {
|
|
68
|
+
store[serverName] = candidate;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return store;
|
|
72
|
+
}
|
|
50
73
|
/**
|
|
51
74
|
* Reads the full tool cache store from disk. Returns an empty object
|
|
52
|
-
* when the file does not exist.
|
|
75
|
+
* when the file does not exist. A corrupted cache file (invalid JSON,
|
|
76
|
+
* a non-object root, or entries without a `tools` array) never throws:
|
|
77
|
+
* it is treated as empty or partially empty so a damaged cache can
|
|
78
|
+
* never break the router. The next write simply rebuilds the file.
|
|
53
79
|
*
|
|
54
80
|
* @param configPath - Absolute path to the mcp.json file.
|
|
55
|
-
* @returns The tool cache store
|
|
56
|
-
* @throws
|
|
81
|
+
* @returns The tool cache store (possibly empty or partial).
|
|
82
|
+
* @throws Only on unexpected I/O errors (e.g. permission denied).
|
|
57
83
|
*/
|
|
58
84
|
async function readCacheStore(configPath) {
|
|
59
85
|
const cachePath = getCachePath(configPath);
|
|
@@ -67,17 +93,36 @@ async function readCacheStore(configPath) {
|
|
|
67
93
|
}
|
|
68
94
|
throw new Error(`Failed to read tool cache file: ${cachePath}`, { cause: err });
|
|
69
95
|
}
|
|
70
|
-
let parsed;
|
|
71
96
|
try {
|
|
72
|
-
|
|
97
|
+
return sanitizeStore(JSON.parse(raw));
|
|
73
98
|
}
|
|
74
|
-
catch
|
|
75
|
-
|
|
99
|
+
catch {
|
|
100
|
+
// Invalid JSON: treat the whole cache as empty rather than throwing.
|
|
101
|
+
// loadToolCache reports "no cache" and saveToolCache rebuilds it.
|
|
102
|
+
return {};
|
|
76
103
|
}
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
}
|
|
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;
|
|
79
125
|
}
|
|
80
|
-
return parsed;
|
|
81
126
|
}
|
|
82
127
|
/**
|
|
83
128
|
* Saves discovered tools to the on-disk tool cache for a single server.
|
|
@@ -98,7 +143,7 @@ export async function saveToolCache(configPath, serverName, tools) {
|
|
|
98
143
|
tools,
|
|
99
144
|
cachedAt: new Date().toISOString(),
|
|
100
145
|
};
|
|
101
|
-
await
|
|
146
|
+
await writeCacheFileAtomic(cachePath, JSON.stringify(store, null, 2) + '\n');
|
|
102
147
|
});
|
|
103
148
|
}
|
|
104
149
|
/**
|
|
@@ -108,8 +153,8 @@ export async function saveToolCache(configPath, serverName, tools) {
|
|
|
108
153
|
*
|
|
109
154
|
* @param configPath - Absolute path to the mcp.json file.
|
|
110
155
|
* @param serverName - The server whose cached tools to load.
|
|
111
|
-
* @returns The cached tool descriptors, or `undefined` when not cached
|
|
112
|
-
*
|
|
156
|
+
* @returns The cached tool descriptors, or `undefined` when not cached
|
|
157
|
+
* (including when the cache file is missing or corrupted).
|
|
113
158
|
*/
|
|
114
159
|
export async function loadToolCache(configPath, serverName) {
|
|
115
160
|
const store = await readCacheStore(configPath);
|
|
@@ -143,7 +188,7 @@ export async function clearToolCache(configPath, serverName) {
|
|
|
143
188
|
await fs.unlink(cachePath).catch(() => { });
|
|
144
189
|
}
|
|
145
190
|
else {
|
|
146
|
-
await
|
|
191
|
+
await writeCacheFileAtomic(cachePath, JSON.stringify(store, null, 2) + '\n');
|
|
147
192
|
}
|
|
148
193
|
});
|
|
149
194
|
}
|