@context-engine-bridge/context-engine-mcp-bridge 0.0.30 → 0.0.32

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 +147 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
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
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { randomUUID } from "node:crypto";
4
+ import { randomUUID, createHash } from "node:crypto";
5
5
  import { execSync } from "node:child_process";
6
6
  import os from "node:os";
7
7
  import http, { createServer } from "node:http";
@@ -15,40 +15,130 @@ import { loadAnyAuthEntry, loadAuthEntry, readConfig, saveAuthEntry } from "./au
15
15
  import { maybeRemapToolArgs, maybeRemapToolResult } from "./resultPathMapping.js";
16
16
  import * as oauthHandler from "./oauthHandler.js";
17
17
 
18
- let _lspConnCache;
19
- let _lspConnCacheTs = 0;
20
18
  const LSP_CONN_CACHE_TTL = 5000;
21
- const LSP_CONN_MAX_AGE = 60 * 60 * 1000;
19
+ const LSP_CONN_MAX_AGE = 24 * 60 * 60 * 1000;
20
+ let _lspConnCache = { value: undefined, ts: 0, key: "" };
22
21
 
23
22
  function _isValidPort(v) {
24
23
  const p = Number.parseInt(String(v), 10);
25
24
  return Number.isFinite(p) && p >= 1024 && p <= 65535;
26
25
  }
27
26
 
28
- function _readLspConnection() {
27
+ function _computeWorkspaceDir(workspacePath) {
28
+ const normalized = path.resolve(workspacePath).replace(/\\/g, "/").toLowerCase();
29
+ const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 12);
30
+ const safeName = path.basename(workspacePath).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 50);
31
+ return path.join(os.homedir(), ".ctxce", "workspaces", `${safeName}-${hash}`);
32
+ }
33
+
34
+ function _tryReadConnFile(connPath, now) {
35
+ try {
36
+ const conn = JSON.parse(fs.readFileSync(connPath, "utf8"));
37
+ if (!conn.port || !_isValidPort(conn.port) || !conn.pid) return null;
38
+ try { process.kill(conn.pid, 0); } catch (_) { return null; }
39
+ if (conn.created_at && (now - conn.created_at) > LSP_CONN_MAX_AGE) return null;
40
+ return { port: String(conn.port), secret: conn.secret || "" };
41
+ } catch (_) {}
42
+ return null;
43
+ }
44
+
45
+ function _readLspConnection(workspace) {
29
46
  const envPort = process.env.CTXCE_LSP_PORT;
30
- if (envPort && _isValidPort(envPort)) return { port: envPort, secret: process.env.CTXCE_LSP_SECRET || "" };
47
+ if (envPort && _isValidPort(envPort)) {
48
+ return { port: envPort, secret: process.env.CTXCE_LSP_SECRET || "" };
49
+ }
50
+
31
51
  const now = Date.now();
32
- if (_lspConnCache !== undefined && (now - _lspConnCacheTs) < LSP_CONN_CACHE_TTL) return _lspConnCache;
52
+ const cacheKey = workspace || "";
53
+ if (_lspConnCache.value !== undefined && (now - _lspConnCache.ts) < LSP_CONN_CACHE_TTL && _lspConnCache.key === cacheKey) {
54
+ return _lspConnCache.value;
55
+ }
33
56
 
34
- function _cacheNull() {
35
- _lspConnCache = null;
36
- _lspConnCacheTs = now;
37
- return null;
57
+ let result = null;
58
+ if (workspace) {
59
+ const wsDir = _computeWorkspaceDir(workspace);
60
+ result = _tryReadConnFile(path.join(wsDir, "lsp-connection.json"), now);
61
+ }
62
+ if (!result) {
63
+ result = _tryReadConnFile(path.join(os.homedir(), ".ctxce", "lsp-connection.json"), now);
38
64
  }
39
65
 
66
+ _lspConnCache = { value: result, ts: now, key: cacheKey };
67
+ return result;
68
+ }
69
+
70
+ const _LSP_ENRICHABLE_TOOLS = new Set([
71
+ "batch_search", "batch_symbol_graph", "batch_graph_query",
72
+ "repo_search", "repo_search_compat", "symbol_graph", "search",
73
+ "code_search", "context_search", "search_tests_for",
74
+ "search_config_for", "search_callers_for", "search_importers_for",
75
+ ]);
76
+
77
+ function _extractPaths(obj, paths, depth = 0) {
78
+ if (!obj || typeof obj !== "object" || depth > 20) return;
79
+ if (Array.isArray(obj)) {
80
+ for (const item of obj) _extractPaths(item, paths, depth + 1);
81
+ return;
82
+ }
83
+ if (typeof obj.path === "string" && (obj.path.startsWith("/") || /^[a-zA-Z]:[/\\]/.test(obj.path))) paths.add(obj.path);
84
+ for (const [key, val] of Object.entries(obj)) {
85
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
86
+ if (val && typeof val === "object") _extractPaths(val, paths, depth + 1);
87
+ }
88
+ }
89
+
90
+ function _callLspHandler(port, secret, operation, params) {
91
+ const postData = JSON.stringify(params);
92
+ return new Promise((resolve) => {
93
+ const req = http.request({
94
+ hostname: "127.0.0.1",
95
+ port: parseInt(port, 10),
96
+ path: `/lsp/${operation}`,
97
+ method: "POST",
98
+ headers: {
99
+ "Content-Type": "application/json",
100
+ "Content-Length": Buffer.byteLength(postData),
101
+ ...(secret ? { "x-lsp-handler-token": secret } : {}),
102
+ },
103
+ timeout: 3000,
104
+ }, (res) => {
105
+ let data = "";
106
+ let exceeded = false;
107
+ const MAX_RESP = 5 * 1024 * 1024;
108
+ res.on("data", chunk => {
109
+ if (exceeded) return;
110
+ data += chunk;
111
+ if (data.length > MAX_RESP) { exceeded = true; req.destroy(); resolve(null); }
112
+ });
113
+ res.on("end", () => { if (!exceeded) { try { resolve(JSON.parse(data)); } catch { resolve(null); } } });
114
+ });
115
+ req.on("error", () => resolve(null));
116
+ req.on("timeout", () => { req.destroy(); resolve(null); });
117
+ req.write(postData);
118
+ req.end();
119
+ });
120
+ }
121
+
122
+ async function _enrichWithLsp(result, lspConn) {
40
123
  try {
41
- const connPath = path.join(os.homedir(), ".ctxce", "lsp-connection.json");
42
- const conn = JSON.parse(fs.readFileSync(connPath, "utf8"));
43
- if (conn.port && _isValidPort(conn.port) && conn.pid) {
44
- try { process.kill(conn.pid, 0); } catch (_) { return _cacheNull(); }
45
- if (conn.created_at && (now - conn.created_at) > LSP_CONN_MAX_AGE) return _cacheNull();
46
- _lspConnCache = { port: String(conn.port), secret: conn.secret || "" };
47
- _lspConnCacheTs = now;
48
- return _lspConnCache;
49
- }
50
- } catch (_) {}
51
- return _cacheNull();
124
+ if (!Array.isArray(result?.content)) return result;
125
+ const textBlock = result.content.find(c => c.type === "text");
126
+ if (!textBlock?.text) return result;
127
+ let parsed;
128
+ try { parsed = JSON.parse(textBlock.text); } catch { return result; }
129
+ 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 };
136
+ textBlock.text = JSON.stringify(parsed);
137
+ return result;
138
+ } catch (err) {
139
+ debugLog("[ctxce] LSP enrichment failed: " + String(err));
140
+ return result;
141
+ }
52
142
  }
53
143
 
54
144
  function debugLog(message) {
@@ -837,8 +927,18 @@ async function createBridgeServer(options) {
837
927
  const memoryTools = await listMemoryTools(memoryClient);
838
928
  const tools = dedupeTools([...indexerTools, ...memoryTools]);
839
929
 
840
- const lspAvailable = !!_readLspConnection();
930
+ const lspAvailable = !!_readLspConnection(workspace);
841
931
  if (lspAvailable) {
932
+ const lspProp = {
933
+ type: "boolean",
934
+ description: "When true, auto-enrich results with live LSP diagnostics (errors/warnings) for files in the response. Zero extra tool calls needed.",
935
+ };
936
+ for (const tool of tools) {
937
+ if (_LSP_ENRICHABLE_TOOLS.has(tool.name) && tool.inputSchema?.properties) {
938
+ tool.inputSchema = { ...tool.inputSchema, properties: { ...tool.inputSchema.properties, include_lsp: lspProp } };
939
+ }
940
+ }
941
+
842
942
  const pathProp = { type: "string", description: "Absolute file path" };
843
943
  const lineProp = { type: "integer", description: "0-based line" };
844
944
  const charProp = { type: "integer", description: "0-based column" };
@@ -920,6 +1020,13 @@ async function createBridgeServer(options) {
920
1020
  const name = params.name;
921
1021
  let args = params.arguments;
922
1022
 
1023
+ let includeLsp = false;
1024
+ if (args && typeof args === "object" && args.include_lsp === true && _LSP_ENRICHABLE_TOOLS.has(name)) {
1025
+ includeLsp = true;
1026
+ const { include_lsp: _stripped, ...rest } = args;
1027
+ args = rest;
1028
+ }
1029
+
923
1030
  debugLog(`[ctxce] tools/call: ${name || "<no-name>"}`);
924
1031
 
925
1032
  // Refresh session before each call; re-init clients if session changes.
@@ -961,7 +1068,7 @@ async function createBridgeServer(options) {
961
1068
  }
962
1069
 
963
1070
  if (name && name.toLowerCase().startsWith("lsp_")) {
964
- const lspConn = _readLspConnection();
1071
+ const lspConn = _readLspConnection(workspace);
965
1072
  const lspPort = lspConn ? lspConn.port : null;
966
1073
  const lspSecret = lspConn ? lspConn.secret : "";
967
1074
  if (!lspPort) {
@@ -973,7 +1080,7 @@ async function createBridgeServer(options) {
973
1080
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Unknown LSP operation" }) }] };
974
1081
  }
975
1082
  try {
976
- const lspArgs = params.arguments || {};
1083
+ const lspArgs = args || {};
977
1084
  const postData = JSON.stringify(lspArgs);
978
1085
  const result = await new Promise((resolve, reject) => {
979
1086
  const req = http.request({
@@ -985,9 +1092,15 @@ async function createBridgeServer(options) {
985
1092
  timeout: 15000,
986
1093
  }, (res) => {
987
1094
  let data = "";
988
- res.on("data", chunk => { data += chunk; });
1095
+ let exceeded = false;
1096
+ const MAX_RESP = 5 * 1024 * 1024;
1097
+ res.on("data", chunk => {
1098
+ if (exceeded) return;
1099
+ data += chunk;
1100
+ if (data.length > MAX_RESP) { exceeded = true; req.destroy(); resolve({ ok: false, error: "Response too large" }); }
1101
+ });
989
1102
  res.on("end", () => {
990
- try { resolve(JSON.parse(data)); } catch { resolve({ ok: false, error: "Invalid response from LSP handler" }); }
1103
+ if (!exceeded) { try { resolve(JSON.parse(data)); } catch { resolve({ ok: false, error: "Invalid response from LSP handler" }); } }
991
1104
  });
992
1105
  });
993
1106
  req.on("error", reject);
@@ -997,7 +1110,9 @@ async function createBridgeServer(options) {
997
1110
  });
998
1111
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
999
1112
  } catch (err) {
1000
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `LSP proxy error: ${err.message || String(err)}` }) }] };
1113
+ debugLog("[ctxce] LSP proxy error: " + String(err));
1114
+ const safeMsg = (err && err.code) ? `LSP proxy error: ${err.code}` : "LSP proxy error: request failed";
1115
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: safeMsg }) }] };
1001
1116
  }
1002
1117
  }
1003
1118
 
@@ -1029,7 +1144,10 @@ async function createBridgeServer(options) {
1029
1144
  undefined,
1030
1145
  { timeout: timeoutMs },
1031
1146
  );
1032
- return maybeRemapToolResult(name, result, workspace);
1147
+ let finalResult = maybeRemapToolResult(name, result, workspace);
1148
+ const lspConn = includeLsp && _readLspConnection(workspace);
1149
+ if (lspConn) finalResult = await _enrichWithLsp(finalResult, lspConn);
1150
+ return finalResult;
1033
1151
  } catch (err) {
1034
1152
  lastError = err;
1035
1153