@context-engine-bridge/context-engine-mcp-bridge 0.0.33 → 0.0.34
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 +145 -9
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;
|
|
@@ -92,7 +120,12 @@ function _extractPaths(obj, paths, workspace, depth = 0) {
|
|
|
92
120
|
}
|
|
93
121
|
if (typeof obj.path === "string" && obj.path.length > 0) {
|
|
94
122
|
if (_isAbsolutePath(obj.path)) {
|
|
95
|
-
|
|
123
|
+
if (workspace) {
|
|
124
|
+
const contained = _resolveAndContain(obj.path, workspace);
|
|
125
|
+
if (contained) paths.add(contained);
|
|
126
|
+
} else {
|
|
127
|
+
paths.add(obj.path);
|
|
128
|
+
}
|
|
96
129
|
} else if (workspace) {
|
|
97
130
|
const contained = _resolveAndContain(obj.path, workspace);
|
|
98
131
|
if (contained) paths.add(contained);
|
|
@@ -148,16 +181,34 @@ async function _enrichWithLsp(result, lspConn, workspace) {
|
|
|
148
181
|
let parsed;
|
|
149
182
|
try { parsed = JSON.parse(textBlock.text); } catch { return result; }
|
|
150
183
|
if (!parsed.ok) return result;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
if (_lspCircuitOpen()) {
|
|
185
|
+
parsed._lsp_status = "circuit_open";
|
|
186
|
+
} else {
|
|
187
|
+
const paths = new Set();
|
|
188
|
+
_extractPaths(parsed, paths, workspace);
|
|
189
|
+
if (paths.size === 0) {
|
|
190
|
+
parsed._lsp_status = "no_paths";
|
|
191
|
+
} else {
|
|
192
|
+
const diag = await _callLspHandler(lspConn.port, lspConn.secret, "diagnostics_recent", { paths: [...paths] });
|
|
193
|
+
if (!diag) {
|
|
194
|
+
_lspCircuitRecordFailure();
|
|
195
|
+
parsed._lsp_status = "connection_failed";
|
|
196
|
+
} else {
|
|
197
|
+
_lspCircuitRecordSuccess();
|
|
198
|
+
if (!diag.ok || !diag.files || diag.total === 0) {
|
|
199
|
+
parsed._lsp_status = "no_diagnostics";
|
|
200
|
+
} else {
|
|
201
|
+
parsed._lsp = { diagnostics: diag.files, total: diag.total };
|
|
202
|
+
parsed._lsp_status = "ok";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
157
207
|
textBlock.text = JSON.stringify(parsed);
|
|
158
208
|
return result;
|
|
159
209
|
} catch (err) {
|
|
160
210
|
debugLog("[ctxce] LSP enrichment failed: " + String(err));
|
|
211
|
+
_lspCircuitRecordFailure();
|
|
161
212
|
return result;
|
|
162
213
|
}
|
|
163
214
|
}
|
|
@@ -586,8 +637,88 @@ async function fetchBridgeCollectionState({
|
|
|
586
637
|
}
|
|
587
638
|
}
|
|
588
639
|
|
|
640
|
+
function _validateWorkspacePath(raw) {
|
|
641
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
642
|
+
const resolved = path.resolve(raw);
|
|
643
|
+
if (!_isAbsolutePath(resolved)) return null;
|
|
644
|
+
try {
|
|
645
|
+
const stat = fs.statSync(resolved);
|
|
646
|
+
if (!stat.isDirectory()) return null;
|
|
647
|
+
} catch (_) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
return resolved;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const MAX_WS_SCAN = 50;
|
|
654
|
+
|
|
655
|
+
function _resolveWorkspace(providedWorkspace) {
|
|
656
|
+
const wsDir = _computeWorkspaceDir(providedWorkspace);
|
|
657
|
+
const now = Date.now();
|
|
658
|
+
const connResult = _tryReadConnFile(path.join(wsDir, "lsp-connection.json"), now);
|
|
659
|
+
if (connResult) return providedWorkspace;
|
|
660
|
+
|
|
661
|
+
const wsRoot = path.join(os.homedir(), ".ctxce", "workspaces");
|
|
662
|
+
try {
|
|
663
|
+
const dirs = fs.readdirSync(wsRoot).slice(0, MAX_WS_SCAN);
|
|
664
|
+
let bestLspMatch = null;
|
|
665
|
+
let bestLspTs = 0;
|
|
666
|
+
let bestMetaMatch = null;
|
|
667
|
+
let bestMetaTs = 0;
|
|
668
|
+
for (const dir of dirs) {
|
|
669
|
+
if (dir === "." || dir === ".." || dir.includes("/") || dir.includes("\0")) continue;
|
|
670
|
+
const dirPath = path.join(wsRoot, dir);
|
|
671
|
+
try {
|
|
672
|
+
const lstat = fs.lstatSync(dirPath);
|
|
673
|
+
if (!lstat.isDirectory()) continue;
|
|
674
|
+
} catch (_) { continue; }
|
|
675
|
+
try {
|
|
676
|
+
const meta = JSON.parse(fs.readFileSync(path.join(dirPath, "meta.json"), "utf8"));
|
|
677
|
+
const wp = _validateWorkspacePath(meta.workspace_path);
|
|
678
|
+
if (!wp) continue;
|
|
679
|
+
const connPath = path.join(dirPath, "lsp-connection.json");
|
|
680
|
+
try {
|
|
681
|
+
const conn = JSON.parse(fs.readFileSync(connPath, "utf8"));
|
|
682
|
+
if (conn.pid && conn.port && _isValidPort(conn.port)) {
|
|
683
|
+
try {
|
|
684
|
+
process.kill(conn.pid, 0);
|
|
685
|
+
if (conn.created_at && (now - conn.created_at) > LSP_CONN_MAX_AGE) {
|
|
686
|
+
} else {
|
|
687
|
+
const connTs = typeof conn.created_at === "number" ? conn.created_at : 0;
|
|
688
|
+
if (connTs > bestLspTs) {
|
|
689
|
+
bestLspTs = connTs;
|
|
690
|
+
bestLspMatch = wp;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} catch (_) {}
|
|
694
|
+
}
|
|
695
|
+
} catch (_) {}
|
|
696
|
+
const updatedAt = typeof meta.updated_at === "number"
|
|
697
|
+
? meta.updated_at
|
|
698
|
+
: typeof meta.updated_at === "string"
|
|
699
|
+
? new Date(meta.updated_at).getTime() || 0
|
|
700
|
+
: 0;
|
|
701
|
+
if (updatedAt > bestMetaTs) {
|
|
702
|
+
bestMetaTs = updatedAt;
|
|
703
|
+
bestMetaMatch = wp;
|
|
704
|
+
}
|
|
705
|
+
} catch (_) {}
|
|
706
|
+
}
|
|
707
|
+
if (bestLspMatch) {
|
|
708
|
+
debugLog("[ctxce] Resolved workspace from active LSP connection");
|
|
709
|
+
return bestLspMatch;
|
|
710
|
+
}
|
|
711
|
+
if (bestMetaMatch) {
|
|
712
|
+
debugLog("[ctxce] Resolved workspace from most recent meta.json");
|
|
713
|
+
return bestMetaMatch;
|
|
714
|
+
}
|
|
715
|
+
} catch (_) {}
|
|
716
|
+
|
|
717
|
+
return providedWorkspace;
|
|
718
|
+
}
|
|
719
|
+
|
|
589
720
|
async function createBridgeServer(options) {
|
|
590
|
-
const workspace = options.workspace || process.cwd();
|
|
721
|
+
const workspace = _resolveWorkspace(options.workspace || process.cwd());
|
|
591
722
|
const indexerUrl = options.indexerUrl;
|
|
592
723
|
const memoryUrl = options.memoryUrl;
|
|
593
724
|
const explicitCollection = options.collection;
|
|
@@ -1089,6 +1220,9 @@ async function createBridgeServer(options) {
|
|
|
1089
1220
|
}
|
|
1090
1221
|
|
|
1091
1222
|
if (name && name.toLowerCase().startsWith("lsp_")) {
|
|
1223
|
+
if (_lspCircuitOpen()) {
|
|
1224
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "LSP proxy temporarily unavailable (circuit breaker open after repeated failures). Will retry automatically." }) }] };
|
|
1225
|
+
}
|
|
1092
1226
|
const lspConn = _readLspConnection(workspace);
|
|
1093
1227
|
const lspPort = lspConn ? lspConn.port : null;
|
|
1094
1228
|
const lspSecret = lspConn ? lspConn.secret : "";
|
|
@@ -1110,7 +1244,7 @@ async function createBridgeServer(options) {
|
|
|
1110
1244
|
path: `/lsp/${operation}`,
|
|
1111
1245
|
method: "POST",
|
|
1112
1246
|
headers: Object.assign({ "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData) }, lspSecret ? { "x-lsp-handler-token": lspSecret } : {}),
|
|
1113
|
-
timeout:
|
|
1247
|
+
timeout: LSP_DIRECT_TIMEOUT_MS,
|
|
1114
1248
|
}, (res) => {
|
|
1115
1249
|
let data = "";
|
|
1116
1250
|
let exceeded = false;
|
|
@@ -1130,8 +1264,10 @@ async function createBridgeServer(options) {
|
|
|
1130
1264
|
req.write(postData);
|
|
1131
1265
|
req.end();
|
|
1132
1266
|
});
|
|
1267
|
+
_lspCircuitRecordSuccess();
|
|
1133
1268
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1134
1269
|
} catch (err) {
|
|
1270
|
+
_lspCircuitRecordFailure();
|
|
1135
1271
|
debugLog("[ctxce] LSP proxy error: " + String(err));
|
|
1136
1272
|
const safeMsg = (err && err.code) ? `LSP proxy error: ${err.code}` : "LSP proxy error: request failed";
|
|
1137
1273
|
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: safeMsg }) }] };
|