@context-engine-bridge/context-engine-mcp-bridge 0.0.32 → 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 +172 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.32",
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;
@@ -74,16 +102,41 @@ const _LSP_ENRICHABLE_TOOLS = new Set([
74
102
  "search_config_for", "search_callers_for", "search_importers_for",
75
103
  ]);
76
104
 
77
- function _extractPaths(obj, paths, depth = 0) {
105
+ function _isAbsolutePath(p) {
106
+ return p.startsWith("/") || /^[a-zA-Z]:[/\\]/.test(p);
107
+ }
108
+
109
+ function _resolveAndContain(relPath, workspace) {
110
+ const resolved = path.resolve(workspace, relPath);
111
+ if (resolved === workspace || resolved.startsWith(workspace + path.sep)) return resolved;
112
+ return null;
113
+ }
114
+
115
+ function _extractPaths(obj, paths, workspace, depth = 0) {
78
116
  if (!obj || typeof obj !== "object" || depth > 20) return;
79
117
  if (Array.isArray(obj)) {
80
- for (const item of obj) _extractPaths(item, paths, depth + 1);
118
+ for (const item of obj) _extractPaths(item, paths, workspace, depth + 1);
81
119
  return;
82
120
  }
83
- if (typeof obj.path === "string" && (obj.path.startsWith("/") || /^[a-zA-Z]:[/\\]/.test(obj.path))) paths.add(obj.path);
121
+ if (typeof obj.path === "string" && obj.path.length > 0) {
122
+ if (_isAbsolutePath(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
+ }
129
+ } else if (workspace) {
130
+ const contained = _resolveAndContain(obj.path, workspace);
131
+ if (contained) paths.add(contained);
132
+ }
133
+ } else if (typeof obj.rel_path === "string" && obj.rel_path.length > 0 && workspace) {
134
+ const contained = _resolveAndContain(obj.rel_path, workspace);
135
+ if (contained) paths.add(contained);
136
+ }
84
137
  for (const [key, val] of Object.entries(obj)) {
85
138
  if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
86
- if (val && typeof val === "object") _extractPaths(val, paths, depth + 1);
139
+ if (val && typeof val === "object") _extractPaths(val, paths, workspace, depth + 1);
87
140
  }
88
141
  }
89
142
 
@@ -105,6 +158,7 @@ function _callLspHandler(port, secret, operation, params) {
105
158
  let data = "";
106
159
  let exceeded = false;
107
160
  const MAX_RESP = 5 * 1024 * 1024;
161
+ res.on("error", () => {});
108
162
  res.on("data", chunk => {
109
163
  if (exceeded) return;
110
164
  data += chunk;
@@ -119,7 +173,7 @@ function _callLspHandler(port, secret, operation, params) {
119
173
  });
120
174
  }
121
175
 
122
- async function _enrichWithLsp(result, lspConn) {
176
+ async function _enrichWithLsp(result, lspConn, workspace) {
123
177
  try {
124
178
  if (!Array.isArray(result?.content)) return result;
125
179
  const textBlock = result.content.find(c => c.type === "text");
@@ -127,16 +181,34 @@ async function _enrichWithLsp(result, lspConn) {
127
181
  let parsed;
128
182
  try { parsed = JSON.parse(textBlock.text); } catch { return result; }
129
183
  if (!parsed.ok) return result;
130
- const paths = new Set();
131
- _extractPaths(parsed, paths);
132
- if (paths.size === 0) return result;
133
- const diag = await _callLspHandler(lspConn.port, lspConn.secret, "diagnostics_recent", { paths: [...paths] });
134
- if (!diag?.ok || !diag.files || diag.total === 0) return result;
135
- 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
+ }
136
207
  textBlock.text = JSON.stringify(parsed);
137
208
  return result;
138
209
  } catch (err) {
139
210
  debugLog("[ctxce] LSP enrichment failed: " + String(err));
211
+ _lspCircuitRecordFailure();
140
212
  return result;
141
213
  }
142
214
  }
@@ -565,8 +637,88 @@ async function fetchBridgeCollectionState({
565
637
  }
566
638
  }
567
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
+
568
720
  async function createBridgeServer(options) {
569
- const workspace = options.workspace || process.cwd();
721
+ const workspace = _resolveWorkspace(options.workspace || process.cwd());
570
722
  const indexerUrl = options.indexerUrl;
571
723
  const memoryUrl = options.memoryUrl;
572
724
  const explicitCollection = options.collection;
@@ -1068,6 +1220,9 @@ async function createBridgeServer(options) {
1068
1220
  }
1069
1221
 
1070
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
+ }
1071
1226
  const lspConn = _readLspConnection(workspace);
1072
1227
  const lspPort = lspConn ? lspConn.port : null;
1073
1228
  const lspSecret = lspConn ? lspConn.secret : "";
@@ -1089,11 +1244,12 @@ async function createBridgeServer(options) {
1089
1244
  path: `/lsp/${operation}`,
1090
1245
  method: "POST",
1091
1246
  headers: Object.assign({ "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData) }, lspSecret ? { "x-lsp-handler-token": lspSecret } : {}),
1092
- timeout: 15000,
1247
+ timeout: LSP_DIRECT_TIMEOUT_MS,
1093
1248
  }, (res) => {
1094
1249
  let data = "";
1095
1250
  let exceeded = false;
1096
1251
  const MAX_RESP = 5 * 1024 * 1024;
1252
+ res.on("error", () => {});
1097
1253
  res.on("data", chunk => {
1098
1254
  if (exceeded) return;
1099
1255
  data += chunk;
@@ -1108,8 +1264,10 @@ async function createBridgeServer(options) {
1108
1264
  req.write(postData);
1109
1265
  req.end();
1110
1266
  });
1267
+ _lspCircuitRecordSuccess();
1111
1268
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
1112
1269
  } catch (err) {
1270
+ _lspCircuitRecordFailure();
1113
1271
  debugLog("[ctxce] LSP proxy error: " + String(err));
1114
1272
  const safeMsg = (err && err.code) ? `LSP proxy error: ${err.code}` : "LSP proxy error: request failed";
1115
1273
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: safeMsg }) }] };
@@ -1146,7 +1304,7 @@ async function createBridgeServer(options) {
1146
1304
  );
1147
1305
  let finalResult = maybeRemapToolResult(name, result, workspace);
1148
1306
  const lspConn = includeLsp && _readLspConnection(workspace);
1149
- if (lspConn) finalResult = await _enrichWithLsp(finalResult, lspConn);
1307
+ if (lspConn) finalResult = await _enrichWithLsp(finalResult, lspConn, workspace);
1150
1308
  return finalResult;
1151
1309
  } catch (err) {
1152
1310
  lastError = err;