@context-engine-bridge/context-engine-mcp-bridge 0.0.52 → 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 +222 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.52",
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
@@ -71,11 +71,6 @@ function _tryReadConnFile(connPath, now) {
71
71
  }
72
72
 
73
73
  function _readLspConnection(workspace) {
74
- const envPort = process.env.CTXCE_LSP_PORT;
75
- if (envPort && _isValidPort(envPort)) {
76
- return { port: envPort, secret: process.env.CTXCE_LSP_SECRET || "" };
77
- }
78
-
79
74
  const now = Date.now();
80
75
  const cacheKey = workspace || "";
81
76
  if (_lspConnCache.value !== undefined && (now - _lspConnCache.ts) < LSP_CONN_CACHE_TTL && _lspConnCache.key === cacheKey) {
@@ -91,6 +86,13 @@ function _readLspConnection(workspace) {
91
86
  result = _tryReadConnFile(path.join(os.homedir(), ".ctxce", "lsp-connection.json"), now);
92
87
  }
93
88
 
89
+ if (!result) {
90
+ const envPort = process.env.CTXCE_LSP_PORT;
91
+ if (envPort && _isValidPort(envPort)) {
92
+ result = { port: envPort, secret: process.env.CTXCE_LSP_SECRET || "" };
93
+ }
94
+ }
95
+
94
96
  _lspConnCache = { value: result, ts: now, key: cacheKey };
95
97
  return result;
96
98
  }
@@ -152,15 +154,18 @@ function _callLspHandler(port, secret, operation, params) {
152
154
  },
153
155
  timeout: 3000,
154
156
  }, (res) => {
155
- let data = "";
157
+ const chunks = [];
158
+ let totalBytes = 0;
156
159
  const MAX_RESP = 5 * 1024 * 1024;
157
160
  res.on("error", () => settle(null));
158
161
  res.on("data", chunk => {
159
162
  if (settled) return;
160
- data += chunk;
161
- 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);
162
167
  });
163
- 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); } } });
164
169
  });
165
170
  req.on("error", () => settle(null));
166
171
  req.on("timeout", () => { req.destroy(); settle(null); });
@@ -1261,17 +1266,20 @@ async function createBridgeServer(options) {
1261
1266
  headers: Object.assign({ "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData) }, lspSecret ? { "x-lsp-handler-token": lspSecret } : {}),
1262
1267
  timeout: LSP_DIRECT_TIMEOUT_MS,
1263
1268
  }, (res) => {
1264
- let data = "";
1269
+ const chunks = [];
1270
+ let totalBytes = 0;
1265
1271
  let exceeded = false;
1266
1272
  const MAX_RESP = 5 * 1024 * 1024;
1267
1273
  res.on("error", () => { if (!exceeded) resolve({ ok: false, error: "LSP response stream error" }); });
1268
1274
  res.on("data", chunk => {
1269
1275
  if (exceeded) return;
1270
- data += chunk;
1271
- 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);
1272
1280
  });
1273
1281
  res.on("end", () => {
1274
- 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" }); } }
1275
1283
  });
1276
1284
  });
1277
1285
  req.on("error", reject);
@@ -1291,6 +1299,17 @@ async function createBridgeServer(options) {
1291
1299
 
1292
1300
  await initializeRemoteClients(false);
1293
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
+
1294
1313
  const timeoutMs = getBridgeToolTimeoutMs();
1295
1314
  const maxAttempts = getBridgeRetryAttempts();
1296
1315
  const retryDelayMs = getBridgeRetryDelayMs();
@@ -1317,6 +1336,53 @@ async function createBridgeServer(options) {
1317
1336
  undefined,
1318
1337
  { timeout: timeoutMs },
1319
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
+
1320
1386
  const lspConn = includeLsp && _readLspConnection(workspace);
1321
1387
  if (includeLsp) {
1322
1388
  debugLog("[ctxce] LSP gate: tool=" + name + " lsp=" + (lspConn ? "port:" + lspConn.port : "null"));
@@ -1738,6 +1804,149 @@ export async function runHttpMcpServer(options) {
1738
1804
  process.on("SIGINT", () => shutdown("SIGINT"));
1739
1805
  }
1740
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
+
1741
1950
  function loadConfig(startDir) {
1742
1951
  try {
1743
1952
  let dir = startDir;