@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/mcpServer.js +145 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
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
@@ -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
- paths.add(obj.path);
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
- const paths = new Set();
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 };
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: 15000,
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 }) }] };