@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/mcpServer.js +153 -19
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.35",
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;
@@ -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
- if (_isAbsolutePath(obj.path)) {
95
- paths.add(obj.path);
96
- } else if (workspace) {
97
- const contained = _resolveAndContain(obj.path, workspace);
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 { return result; }
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
- 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 };
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
- if (args && typeof args === "object" && args.include_lsp === true && _LSP_ENRICHABLE_TOOLS.has(name)) {
1046
- includeLsp = true;
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: 15000,
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 (lspConn) finalResult = await _enrichWithLsp(finalResult, lspConn, workspace);
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;