@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.
- package/package.json +1 -1
- package/src/mcpServer.js +222 -13
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1271
|
-
|
|
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(
|
|
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;
|