@context-engine-bridge/context-engine-mcp-bridge 0.0.53 → 0.0.55
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/package.json +1 -1
- package/src/mcpServer.js +216 -8
package/package.json
CHANGED
package/src/mcpServer.js
CHANGED
|
@@ -154,15 +154,18 @@ function _callLspHandler(port, secret, operation, params) {
|
|
|
154
154
|
},
|
|
155
155
|
timeout: 3000,
|
|
156
156
|
}, (res) => {
|
|
157
|
-
|
|
157
|
+
const chunks = [];
|
|
158
|
+
let totalBytes = 0;
|
|
158
159
|
const MAX_RESP = 5 * 1024 * 1024;
|
|
159
160
|
res.on("error", () => settle(null));
|
|
160
161
|
res.on("data", chunk => {
|
|
161
162
|
if (settled) return;
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
164
|
+
totalBytes += buf.length;
|
|
165
|
+
if (totalBytes > MAX_RESP) { req.destroy(); settle(null); return; }
|
|
166
|
+
chunks.push(buf);
|
|
164
167
|
});
|
|
165
|
-
res.on("end", () => { if (!settled) { try { settle(JSON.parse(
|
|
168
|
+
res.on("end", () => { if (!settled) { try { settle(JSON.parse(Buffer.concat(chunks).toString("utf8"))); } catch { settle(null); } } });
|
|
166
169
|
});
|
|
167
170
|
req.on("error", () => settle(null));
|
|
168
171
|
req.on("timeout", () => { req.destroy(); settle(null); });
|
|
@@ -1263,17 +1266,20 @@ async function createBridgeServer(options) {
|
|
|
1263
1266
|
headers: Object.assign({ "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData) }, lspSecret ? { "x-lsp-handler-token": lspSecret } : {}),
|
|
1264
1267
|
timeout: LSP_DIRECT_TIMEOUT_MS,
|
|
1265
1268
|
}, (res) => {
|
|
1266
|
-
|
|
1269
|
+
const chunks = [];
|
|
1270
|
+
let totalBytes = 0;
|
|
1267
1271
|
let exceeded = false;
|
|
1268
1272
|
const MAX_RESP = 5 * 1024 * 1024;
|
|
1269
1273
|
res.on("error", () => { if (!exceeded) resolve({ ok: false, error: "LSP response stream error" }); });
|
|
1270
1274
|
res.on("data", chunk => {
|
|
1271
1275
|
if (exceeded) return;
|
|
1272
|
-
|
|
1273
|
-
|
|
1276
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1277
|
+
totalBytes += buf.length;
|
|
1278
|
+
if (totalBytes > MAX_RESP) { exceeded = true; req.destroy(); resolve({ ok: false, error: "Response too large" }); return; }
|
|
1279
|
+
chunks.push(buf);
|
|
1274
1280
|
});
|
|
1275
1281
|
res.on("end", () => {
|
|
1276
|
-
if (!exceeded) { try { resolve(JSON.parse(
|
|
1282
|
+
if (!exceeded) { try { resolve(JSON.parse(Buffer.concat(chunks).toString("utf8"))); } catch { resolve({ ok: false, error: "Invalid response from LSP handler" }); } }
|
|
1277
1283
|
});
|
|
1278
1284
|
});
|
|
1279
1285
|
req.on("error", reject);
|
|
@@ -1293,6 +1299,18 @@ async function createBridgeServer(options) {
|
|
|
1293
1299
|
|
|
1294
1300
|
await initializeRemoteClients(false);
|
|
1295
1301
|
|
|
1302
|
+
const MAX_FANOUT_COLLECTIONS = 5;
|
|
1303
|
+
const primaryCollection = args && typeof args.collection === "string" ? args.collection : "";
|
|
1304
|
+
const primaryLower = primaryCollection.toLowerCase();
|
|
1305
|
+
const additionalCollections = _FANOUT_TOOLS.has(name)
|
|
1306
|
+
? _loadSessionCollections(workspace).filter(c => c.toLowerCase() !== primaryLower).slice(0, MAX_FANOUT_COLLECTIONS)
|
|
1307
|
+
: [];
|
|
1308
|
+
const shouldFanOut = additionalCollections.length > 0 && indexerClient;
|
|
1309
|
+
|
|
1310
|
+
if (shouldFanOut) {
|
|
1311
|
+
debugLog(`[ctxce] Fan-out: ${name} across ${additionalCollections.length} additional collection(s)`);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1296
1314
|
const timeoutMs = getBridgeToolTimeoutMs();
|
|
1297
1315
|
const maxAttempts = getBridgeRetryAttempts();
|
|
1298
1316
|
const retryDelayMs = getBridgeRetryDelayMs();
|
|
@@ -1319,6 +1337,53 @@ async function createBridgeServer(options) {
|
|
|
1319
1337
|
undefined,
|
|
1320
1338
|
{ timeout: timeoutMs },
|
|
1321
1339
|
);
|
|
1340
|
+
|
|
1341
|
+
if (shouldFanOut) {
|
|
1342
|
+
const fanOutTimeoutMs = Math.min(timeoutMs, 10000);
|
|
1343
|
+
const fanOutPromises = additionalCollections.map(col => {
|
|
1344
|
+
const colArgs = { ...args, collection: col };
|
|
1345
|
+
return targetClient.callTool(
|
|
1346
|
+
{ name, arguments: colArgs },
|
|
1347
|
+
undefined,
|
|
1348
|
+
{ timeout: fanOutTimeoutMs },
|
|
1349
|
+
).catch(err => {
|
|
1350
|
+
debugLog(`[ctxce] Fan-out to ${col} failed: ${String(err)}`);
|
|
1351
|
+
return null;
|
|
1352
|
+
});
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
let fanOutTimer;
|
|
1356
|
+
const fanOutDeadline = new Promise(resolve => { fanOutTimer = setTimeout(() => resolve(null), fanOutTimeoutMs); });
|
|
1357
|
+
const fanOutResults = await Promise.race([
|
|
1358
|
+
Promise.all(fanOutPromises),
|
|
1359
|
+
fanOutDeadline.then(() => fanOutPromises.map(() => null)),
|
|
1360
|
+
]);
|
|
1361
|
+
clearTimeout(fanOutTimer);
|
|
1362
|
+
const additionalTexts = [];
|
|
1363
|
+
for (const fr of fanOutResults) {
|
|
1364
|
+
if (!fr) continue;
|
|
1365
|
+
const tb = Array.isArray(fr.content) && fr.content.find(c => c.type === "text");
|
|
1366
|
+
if (tb && tb.text) additionalTexts.push(tb.text);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if (additionalTexts.length > 0) {
|
|
1370
|
+
const primaryTb = Array.isArray(result.content) && result.content.find(c => c.type === "text");
|
|
1371
|
+
if (primaryTb && primaryTb.text) {
|
|
1372
|
+
if (args && args.limit && !_isBatchTool(name)) {
|
|
1373
|
+
try {
|
|
1374
|
+
const pObj = JSON.parse(primaryTb.text);
|
|
1375
|
+
if (pObj && pObj.ok) { pObj._original_limit = Number(args.limit); primaryTb.text = JSON.stringify(pObj); }
|
|
1376
|
+
} catch (_) {}
|
|
1377
|
+
}
|
|
1378
|
+
const mergeFn = _isBatchTool(name) ? _mergeBatchResults : _mergeResults;
|
|
1379
|
+
primaryTb.text = mergeFn(primaryTb.text, additionalTexts);
|
|
1380
|
+
if (result.structuredContent) {
|
|
1381
|
+
try { result.structuredContent = JSON.parse(primaryTb.text); } catch (_) {}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1322
1387
|
const lspConn = includeLsp && _readLspConnection(workspace);
|
|
1323
1388
|
if (includeLsp) {
|
|
1324
1389
|
debugLog("[ctxce] LSP gate: tool=" + name + " lsp=" + (lspConn ? "port:" + lspConn.port : "null"));
|
|
@@ -1740,6 +1805,149 @@ export async function runHttpMcpServer(options) {
|
|
|
1740
1805
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
1741
1806
|
}
|
|
1742
1807
|
|
|
1808
|
+
const _FANOUT_TOOLS = new Set([
|
|
1809
|
+
"search", "repo_search", "code_search", "context_search",
|
|
1810
|
+
"symbol_graph", "pattern_search",
|
|
1811
|
+
"search_tests_for", "search_config_for", "search_callers_for",
|
|
1812
|
+
"search_importers_for", "search_commits_for",
|
|
1813
|
+
"batch_search", "batch_graph_query", "batch_symbol_graph",
|
|
1814
|
+
]);
|
|
1815
|
+
|
|
1816
|
+
let _sessionCollectionsCache = null;
|
|
1817
|
+
let _sessionCollectionsMtime = 0;
|
|
1818
|
+
let _sessionCollectionsCacheKey = "";
|
|
1819
|
+
|
|
1820
|
+
function _loadSessionCollections(workspacePath) {
|
|
1821
|
+
if (!workspacePath) return [];
|
|
1822
|
+
try {
|
|
1823
|
+
const wsDir = _computeWorkspaceDir(workspacePath);
|
|
1824
|
+
const filePath = path.join(wsDir, "session_collections.json");
|
|
1825
|
+
let mtime = 0;
|
|
1826
|
+
try {
|
|
1827
|
+
mtime = fs.statSync(filePath).mtimeMs;
|
|
1828
|
+
} catch (_) {
|
|
1829
|
+
_sessionCollectionsCache = null;
|
|
1830
|
+
_sessionCollectionsMtime = 0;
|
|
1831
|
+
_sessionCollectionsCacheKey = "";
|
|
1832
|
+
return [];
|
|
1833
|
+
}
|
|
1834
|
+
if (_sessionCollectionsCache && mtime === _sessionCollectionsMtime && _sessionCollectionsCacheKey === workspacePath) {
|
|
1835
|
+
return _sessionCollectionsCache;
|
|
1836
|
+
}
|
|
1837
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
1838
|
+
const parsed = JSON.parse(raw);
|
|
1839
|
+
if (Array.isArray(parsed)) {
|
|
1840
|
+
_sessionCollectionsCache = parsed.filter(c => c && typeof c === "string" && c.length <= 128 && /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(c));
|
|
1841
|
+
_sessionCollectionsMtime = mtime;
|
|
1842
|
+
_sessionCollectionsCacheKey = workspacePath;
|
|
1843
|
+
return _sessionCollectionsCache;
|
|
1844
|
+
}
|
|
1845
|
+
return [];
|
|
1846
|
+
} catch (_) {
|
|
1847
|
+
return [];
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function _isBatchTool(name) {
|
|
1852
|
+
return name && name.startsWith("batch_");
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
function _mergeResults(primaryText, additionalTexts) {
|
|
1856
|
+
try {
|
|
1857
|
+
const primary = JSON.parse(primaryText);
|
|
1858
|
+
if (!primary || !primary.ok) return primaryText;
|
|
1859
|
+
|
|
1860
|
+
for (const addText of additionalTexts) {
|
|
1861
|
+
try {
|
|
1862
|
+
const additional = JSON.parse(addText);
|
|
1863
|
+
if (!additional || !additional.ok) continue;
|
|
1864
|
+
|
|
1865
|
+
if (Array.isArray(primary.results) && Array.isArray(additional.results)) {
|
|
1866
|
+
const existingPaths = new Set(
|
|
1867
|
+
primary.results.map(r => `${r.path || ""}:${r.start_line || 0}:${r.symbol || ""}`)
|
|
1868
|
+
);
|
|
1869
|
+
for (const r of additional.results) {
|
|
1870
|
+
const key = `${r.path || ""}:${r.start_line || 0}:${r.symbol || ""}`;
|
|
1871
|
+
if (!existingPaths.has(key)) {
|
|
1872
|
+
primary.results.push(r);
|
|
1873
|
+
existingPaths.add(key);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (typeof primary.total === "number" && typeof additional.total === "number") {
|
|
1879
|
+
primary.total = primary.results ? primary.results.length : primary.total + additional.total;
|
|
1880
|
+
}
|
|
1881
|
+
if (typeof primary.count === "number" && typeof additional.count === "number") {
|
|
1882
|
+
primary.count = primary.results ? primary.results.length : primary.count + additional.count;
|
|
1883
|
+
}
|
|
1884
|
+
} catch (_) {}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
if (Array.isArray(primary.results) && primary.results.length > 1) {
|
|
1888
|
+
primary.results.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
const originalLimit = primary._original_limit;
|
|
1892
|
+
if (originalLimit && Array.isArray(primary.results) && primary.results.length > originalLimit) {
|
|
1893
|
+
primary.results = primary.results.slice(0, originalLimit);
|
|
1894
|
+
if (typeof primary.total === "number") primary.total = primary.results.length;
|
|
1895
|
+
if (typeof primary.count === "number") primary.count = primary.results.length;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
primary._cross_collection = true;
|
|
1899
|
+
return JSON.stringify(primary);
|
|
1900
|
+
} catch (_) {
|
|
1901
|
+
return primaryText;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function _mergeBatchResults(primaryText, additionalTexts) {
|
|
1906
|
+
try {
|
|
1907
|
+
const primary = JSON.parse(primaryText);
|
|
1908
|
+
if (!primary || !primary.ok || !Array.isArray(primary.results)) return primaryText;
|
|
1909
|
+
|
|
1910
|
+
for (const addText of additionalTexts) {
|
|
1911
|
+
try {
|
|
1912
|
+
const additional = JSON.parse(addText);
|
|
1913
|
+
if (!additional || !additional.ok || !Array.isArray(additional.results)) continue;
|
|
1914
|
+
|
|
1915
|
+
for (let i = 0; i < primary.results.length && i < additional.results.length; i++) {
|
|
1916
|
+
const pItem = primary.results[i];
|
|
1917
|
+
const aItem = additional.results[i];
|
|
1918
|
+
if (!pItem || !aItem) continue;
|
|
1919
|
+
|
|
1920
|
+
const pInner = pItem.result || pItem;
|
|
1921
|
+
const aInner = aItem.result || aItem;
|
|
1922
|
+
|
|
1923
|
+
if (Array.isArray(pInner.results) && Array.isArray(aInner.results)) {
|
|
1924
|
+
const existingPaths = new Set(
|
|
1925
|
+
pInner.results.map(r => `${r.path || ""}:${r.start_line || 0}:${r.symbol || ""}`)
|
|
1926
|
+
);
|
|
1927
|
+
for (const r of aInner.results) {
|
|
1928
|
+
const key = `${r.path || ""}:${r.start_line || 0}:${r.symbol || ""}`;
|
|
1929
|
+
if (!existingPaths.has(key)) {
|
|
1930
|
+
pInner.results.push(r);
|
|
1931
|
+
existingPaths.add(key);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
if (pInner.results.length > 1) {
|
|
1935
|
+
pInner.results.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
1936
|
+
}
|
|
1937
|
+
if (typeof pInner.total === "number") pInner.total = pInner.results.length;
|
|
1938
|
+
if (typeof pInner.count === "number") pInner.count = pInner.results.length;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
} catch (_) {}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
primary._cross_collection = true;
|
|
1945
|
+
return JSON.stringify(primary);
|
|
1946
|
+
} catch (_) {
|
|
1947
|
+
return primaryText;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1743
1951
|
function loadConfig(startDir) {
|
|
1744
1952
|
try {
|
|
1745
1953
|
let dir = startDir;
|