@context-engine-bridge/context-engine-mcp-bridge 0.0.33 → 0.0.35
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 +153 -19
package/package.json
CHANGED
package/src/mcpServer.js
CHANGED
|
@@ -19,6 +19,34 @@ const LSP_CONN_CACHE_TTL = 5000;
|
|
|
19
19
|
const LSP_CONN_MAX_AGE = 24 * 60 * 60 * 1000;
|
|
20
20
|
let _lspConnCache = { value: undefined, ts: 0, key: "" };
|
|
21
21
|
|
|
22
|
+
const LSP_CB_THRESHOLD = 3;
|
|
23
|
+
const LSP_CB_COOLDOWN_MS = 30000;
|
|
24
|
+
const LSP_DIRECT_TIMEOUT_MS = 8000;
|
|
25
|
+
let _lspCircuitBreaker = { failures: 0, openUntil: 0 };
|
|
26
|
+
|
|
27
|
+
function _lspCircuitOpen() {
|
|
28
|
+
if (_lspCircuitBreaker.failures < LSP_CB_THRESHOLD) return false;
|
|
29
|
+
if (Date.now() >= _lspCircuitBreaker.openUntil) {
|
|
30
|
+
_lspCircuitBreaker.failures = 0;
|
|
31
|
+
_lspCircuitBreaker.openUntil = 0;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _lspCircuitRecordSuccess() {
|
|
38
|
+
_lspCircuitBreaker.failures = 0;
|
|
39
|
+
_lspCircuitBreaker.openUntil = 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _lspCircuitRecordFailure() {
|
|
43
|
+
_lspCircuitBreaker.failures = Math.min(_lspCircuitBreaker.failures + 1, LSP_CB_THRESHOLD);
|
|
44
|
+
if (_lspCircuitBreaker.failures >= LSP_CB_THRESHOLD) {
|
|
45
|
+
_lspCircuitBreaker.openUntil = Date.now() + LSP_CB_COOLDOWN_MS;
|
|
46
|
+
debugLog(`[ctxce] LSP circuit breaker open for ${LSP_CB_COOLDOWN_MS}ms after ${_lspCircuitBreaker.failures} consecutive failures`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
22
50
|
function _isValidPort(v) {
|
|
23
51
|
const p = Number.parseInt(String(v), 10);
|
|
24
52
|
return Number.isFinite(p) && p >= 1024 && p <= 65535;
|
|
@@ -90,16 +118,19 @@ function _extractPaths(obj, paths, workspace, depth = 0) {
|
|
|
90
118
|
for (const item of obj) _extractPaths(item, paths, workspace, depth + 1);
|
|
91
119
|
return;
|
|
92
120
|
}
|
|
121
|
+
let rawPath = null;
|
|
93
122
|
if (typeof obj.path === "string" && obj.path.length > 0) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
123
|
+
rawPath = obj.path;
|
|
124
|
+
} else if (typeof obj.rel_path === "string" && obj.rel_path.length > 0) {
|
|
125
|
+
rawPath = obj.rel_path;
|
|
126
|
+
}
|
|
127
|
+
if (rawPath) {
|
|
128
|
+
if (workspace) {
|
|
129
|
+
const contained = _resolveAndContain(rawPath, workspace);
|
|
98
130
|
if (contained) paths.add(contained);
|
|
131
|
+
} else if (_isAbsolutePath(rawPath)) {
|
|
132
|
+
paths.add(rawPath);
|
|
99
133
|
}
|
|
100
|
-
} else if (typeof obj.rel_path === "string" && obj.rel_path.length > 0 && workspace) {
|
|
101
|
-
const contained = _resolveAndContain(obj.rel_path, workspace);
|
|
102
|
-
if (contained) paths.add(contained);
|
|
103
134
|
}
|
|
104
135
|
for (const [key, val] of Object.entries(obj)) {
|
|
105
136
|
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
@@ -142,26 +173,42 @@ function _callLspHandler(port, secret, operation, params) {
|
|
|
142
173
|
|
|
143
174
|
async function _enrichWithLsp(result, lspConn, workspace) {
|
|
144
175
|
try {
|
|
176
|
+
debugLog("[ctxce] LSP enrich: workspace=" + workspace + " port=" + lspConn?.port);
|
|
145
177
|
if (!Array.isArray(result?.content)) return result;
|
|
146
178
|
const textBlock = result.content.find(c => c.type === "text");
|
|
147
179
|
if (!textBlock?.text) return result;
|
|
148
180
|
let parsed;
|
|
149
|
-
try { parsed = JSON.parse(textBlock.text); } catch {
|
|
181
|
+
try { parsed = JSON.parse(textBlock.text); } catch (e) {
|
|
182
|
+
debugLog("[ctxce] LSP enrich: JSON parse failed: " + String(e));
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
150
185
|
if (!parsed.ok) return result;
|
|
151
|
-
|
|
152
|
-
_extractPaths(parsed, paths, workspace);
|
|
153
|
-
if (paths.size === 0) return result;
|
|
154
|
-
const diag = await _callLspHandler(lspConn.port, lspConn.secret, "diagnostics_recent", { paths: [...paths] });
|
|
155
|
-
if (!diag?.ok || !diag.files || diag.total === 0) return result;
|
|
156
|
-
parsed._lsp = { diagnostics: diag.files, total: diag.total };
|
|
186
|
+
parsed._lsp_status = await _resolveLspStatus(parsed, lspConn, workspace);
|
|
157
187
|
textBlock.text = JSON.stringify(parsed);
|
|
158
188
|
return result;
|
|
159
189
|
} catch (err) {
|
|
160
190
|
debugLog("[ctxce] LSP enrichment failed: " + String(err));
|
|
191
|
+
_lspCircuitRecordFailure();
|
|
161
192
|
return result;
|
|
162
193
|
}
|
|
163
194
|
}
|
|
164
195
|
|
|
196
|
+
async function _resolveLspStatus(parsed, lspConn, workspace) {
|
|
197
|
+
if (_lspCircuitOpen()) return "circuit_open";
|
|
198
|
+
const paths = new Set();
|
|
199
|
+
_extractPaths(parsed, paths, workspace);
|
|
200
|
+
if (paths.size === 0) return "no_paths";
|
|
201
|
+
const diag = await _callLspHandler(lspConn.port, lspConn.secret, "diagnostics_recent", { paths: [...paths] });
|
|
202
|
+
if (!diag) {
|
|
203
|
+
_lspCircuitRecordFailure();
|
|
204
|
+
return "connection_failed";
|
|
205
|
+
}
|
|
206
|
+
_lspCircuitRecordSuccess();
|
|
207
|
+
if (!diag.ok || !diag.files || diag.total === 0) return "no_diagnostics";
|
|
208
|
+
parsed._lsp = { diagnostics: diag.files, total: diag.total };
|
|
209
|
+
return "ok";
|
|
210
|
+
}
|
|
211
|
+
|
|
165
212
|
function debugLog(message) {
|
|
166
213
|
try {
|
|
167
214
|
const text = typeof message === "string" ? message : String(message);
|
|
@@ -586,8 +633,73 @@ async function fetchBridgeCollectionState({
|
|
|
586
633
|
}
|
|
587
634
|
}
|
|
588
635
|
|
|
636
|
+
function _validateWorkspacePath(raw) {
|
|
637
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
638
|
+
const resolved = path.resolve(raw);
|
|
639
|
+
if (!_isAbsolutePath(resolved)) return null;
|
|
640
|
+
try {
|
|
641
|
+
const stat = fs.statSync(resolved);
|
|
642
|
+
if (!stat.isDirectory()) return null;
|
|
643
|
+
} catch (_) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
return resolved;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const MAX_WS_SCAN = 50;
|
|
650
|
+
|
|
651
|
+
function _resolveWorkspace(providedWorkspace) {
|
|
652
|
+
const wsDir = _computeWorkspaceDir(providedWorkspace);
|
|
653
|
+
const now = Date.now();
|
|
654
|
+
const connResult = _tryReadConnFile(path.join(wsDir, "lsp-connection.json"), now);
|
|
655
|
+
if (connResult) return providedWorkspace;
|
|
656
|
+
|
|
657
|
+
const wsRoot = path.join(os.homedir(), ".ctxce", "workspaces");
|
|
658
|
+
try {
|
|
659
|
+
const dirs = fs.readdirSync(wsRoot).slice(0, MAX_WS_SCAN);
|
|
660
|
+
let bestMatch = null;
|
|
661
|
+
let bestTs = 0;
|
|
662
|
+
for (const dir of dirs) {
|
|
663
|
+
if (dir === "." || dir === ".." || dir.includes("/") || dir.includes("\0")) continue;
|
|
664
|
+
const dirPath = path.join(wsRoot, dir);
|
|
665
|
+
try {
|
|
666
|
+
const lstat = fs.lstatSync(dirPath);
|
|
667
|
+
if (!lstat.isDirectory()) continue;
|
|
668
|
+
} catch (_) { continue; }
|
|
669
|
+
try {
|
|
670
|
+
const meta = JSON.parse(fs.readFileSync(path.join(dirPath, "meta.json"), "utf8"));
|
|
671
|
+
const wp = _validateWorkspacePath(meta.workspace_path);
|
|
672
|
+
if (!wp) continue;
|
|
673
|
+
const connPath = path.join(dirPath, "lsp-connection.json");
|
|
674
|
+
try {
|
|
675
|
+
const conn = JSON.parse(fs.readFileSync(connPath, "utf8"));
|
|
676
|
+
if (conn.pid && conn.port && _isValidPort(conn.port)) {
|
|
677
|
+
try {
|
|
678
|
+
process.kill(conn.pid, 0);
|
|
679
|
+
const notExpired = !conn.created_at || (now - conn.created_at) <= LSP_CONN_MAX_AGE;
|
|
680
|
+
if (notExpired) {
|
|
681
|
+
const connTs = typeof conn.created_at === "number" ? conn.created_at : 0;
|
|
682
|
+
if (connTs > bestTs) {
|
|
683
|
+
bestTs = connTs;
|
|
684
|
+
bestMatch = wp;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} catch (_) {}
|
|
688
|
+
}
|
|
689
|
+
} catch (_) {}
|
|
690
|
+
} catch (_) {}
|
|
691
|
+
}
|
|
692
|
+
if (bestMatch) {
|
|
693
|
+
debugLog("[ctxce] Resolved workspace from active LSP connection");
|
|
694
|
+
return bestMatch;
|
|
695
|
+
}
|
|
696
|
+
} catch (_) {}
|
|
697
|
+
|
|
698
|
+
return providedWorkspace;
|
|
699
|
+
}
|
|
700
|
+
|
|
589
701
|
async function createBridgeServer(options) {
|
|
590
|
-
const workspace = options.workspace || process.cwd();
|
|
702
|
+
const workspace = _resolveWorkspace(options.workspace || process.cwd());
|
|
591
703
|
const indexerUrl = options.indexerUrl;
|
|
592
704
|
const memoryUrl = options.memoryUrl;
|
|
593
705
|
const explicitCollection = options.collection;
|
|
@@ -1042,11 +1154,14 @@ async function createBridgeServer(options) {
|
|
|
1042
1154
|
let args = params.arguments;
|
|
1043
1155
|
|
|
1044
1156
|
let includeLsp = false;
|
|
1045
|
-
|
|
1046
|
-
|
|
1157
|
+
const rawLsp = args && typeof args === "object" ? args.include_lsp : undefined;
|
|
1158
|
+
if (rawLsp !== undefined) {
|
|
1047
1159
|
const { include_lsp: _stripped, ...rest } = args;
|
|
1048
1160
|
args = rest;
|
|
1049
1161
|
}
|
|
1162
|
+
if ((rawLsp === true || rawLsp === "true" || rawLsp === 1 || rawLsp === "1") && _LSP_ENRICHABLE_TOOLS.has(name)) {
|
|
1163
|
+
includeLsp = true;
|
|
1164
|
+
}
|
|
1050
1165
|
|
|
1051
1166
|
debugLog(`[ctxce] tools/call: ${name || "<no-name>"}`);
|
|
1052
1167
|
|
|
@@ -1089,6 +1204,9 @@ async function createBridgeServer(options) {
|
|
|
1089
1204
|
}
|
|
1090
1205
|
|
|
1091
1206
|
if (name && name.toLowerCase().startsWith("lsp_")) {
|
|
1207
|
+
if (_lspCircuitOpen()) {
|
|
1208
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "LSP proxy temporarily unavailable (circuit breaker open after repeated failures). Will retry automatically." }) }] };
|
|
1209
|
+
}
|
|
1092
1210
|
const lspConn = _readLspConnection(workspace);
|
|
1093
1211
|
const lspPort = lspConn ? lspConn.port : null;
|
|
1094
1212
|
const lspSecret = lspConn ? lspConn.secret : "";
|
|
@@ -1110,7 +1228,7 @@ async function createBridgeServer(options) {
|
|
|
1110
1228
|
path: `/lsp/${operation}`,
|
|
1111
1229
|
method: "POST",
|
|
1112
1230
|
headers: Object.assign({ "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData) }, lspSecret ? { "x-lsp-handler-token": lspSecret } : {}),
|
|
1113
|
-
timeout:
|
|
1231
|
+
timeout: LSP_DIRECT_TIMEOUT_MS,
|
|
1114
1232
|
}, (res) => {
|
|
1115
1233
|
let data = "";
|
|
1116
1234
|
let exceeded = false;
|
|
@@ -1130,8 +1248,10 @@ async function createBridgeServer(options) {
|
|
|
1130
1248
|
req.write(postData);
|
|
1131
1249
|
req.end();
|
|
1132
1250
|
});
|
|
1251
|
+
_lspCircuitRecordSuccess();
|
|
1133
1252
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1134
1253
|
} catch (err) {
|
|
1254
|
+
_lspCircuitRecordFailure();
|
|
1135
1255
|
debugLog("[ctxce] LSP proxy error: " + String(err));
|
|
1136
1256
|
const safeMsg = (err && err.code) ? `LSP proxy error: ${err.code}` : "LSP proxy error: request failed";
|
|
1137
1257
|
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: safeMsg }) }] };
|
|
@@ -1168,7 +1288,21 @@ async function createBridgeServer(options) {
|
|
|
1168
1288
|
);
|
|
1169
1289
|
let finalResult = maybeRemapToolResult(name, result, workspace);
|
|
1170
1290
|
const lspConn = includeLsp && _readLspConnection(workspace);
|
|
1171
|
-
if (
|
|
1291
|
+
if (includeLsp) {
|
|
1292
|
+
debugLog("[ctxce] LSP gate: tool=" + name + " lsp=" + (lspConn ? "port:" + lspConn.port : "null"));
|
|
1293
|
+
}
|
|
1294
|
+
if (lspConn) {
|
|
1295
|
+
finalResult = await _enrichWithLsp(finalResult, lspConn, workspace);
|
|
1296
|
+
} else if (includeLsp) {
|
|
1297
|
+
try {
|
|
1298
|
+
const tb = Array.isArray(finalResult?.content) && finalResult.content.find(c => c.type === "text");
|
|
1299
|
+
if (tb?.text) {
|
|
1300
|
+
const p = JSON.parse(tb.text);
|
|
1301
|
+
p._lsp_status = "not_available";
|
|
1302
|
+
tb.text = JSON.stringify(p);
|
|
1303
|
+
}
|
|
1304
|
+
} catch (_) {}
|
|
1305
|
+
}
|
|
1172
1306
|
return finalResult;
|
|
1173
1307
|
} catch (err) {
|
|
1174
1308
|
lastError = err;
|