@context-engine-bridge/context-engine-mcp-bridge 0.0.53 → 0.0.54

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