@context-engine-bridge/context-engine-mcp-bridge 0.0.31 → 0.0.33

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 +72 -34
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
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,56 @@ 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
 
40
- 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();
66
+ _lspConnCache = { value: result, ts: now, key: cacheKey };
67
+ return result;
52
68
  }
53
69
 
54
70
  const _LSP_ENRICHABLE_TOOLS = new Set([
@@ -58,16 +74,36 @@ const _LSP_ENRICHABLE_TOOLS = new Set([
58
74
  "search_config_for", "search_callers_for", "search_importers_for",
59
75
  ]);
60
76
 
61
- function _extractPaths(obj, paths, depth = 0) {
77
+ function _isAbsolutePath(p) {
78
+ return p.startsWith("/") || /^[a-zA-Z]:[/\\]/.test(p);
79
+ }
80
+
81
+ function _resolveAndContain(relPath, workspace) {
82
+ const resolved = path.resolve(workspace, relPath);
83
+ if (resolved === workspace || resolved.startsWith(workspace + path.sep)) return resolved;
84
+ return null;
85
+ }
86
+
87
+ function _extractPaths(obj, paths, workspace, depth = 0) {
62
88
  if (!obj || typeof obj !== "object" || depth > 20) return;
63
89
  if (Array.isArray(obj)) {
64
- for (const item of obj) _extractPaths(item, paths, depth + 1);
90
+ for (const item of obj) _extractPaths(item, paths, workspace, depth + 1);
65
91
  return;
66
92
  }
67
- if (typeof obj.path === "string" && (obj.path.startsWith("/") || /^[a-zA-Z]:[/\\]/.test(obj.path))) paths.add(obj.path);
93
+ 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);
98
+ if (contained) paths.add(contained);
99
+ }
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
+ }
68
104
  for (const [key, val] of Object.entries(obj)) {
69
105
  if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
70
- if (val && typeof val === "object") _extractPaths(val, paths, depth + 1);
106
+ if (val && typeof val === "object") _extractPaths(val, paths, workspace, depth + 1);
71
107
  }
72
108
  }
73
109
 
@@ -89,6 +125,7 @@ function _callLspHandler(port, secret, operation, params) {
89
125
  let data = "";
90
126
  let exceeded = false;
91
127
  const MAX_RESP = 5 * 1024 * 1024;
128
+ res.on("error", () => {});
92
129
  res.on("data", chunk => {
93
130
  if (exceeded) return;
94
131
  data += chunk;
@@ -103,7 +140,7 @@ function _callLspHandler(port, secret, operation, params) {
103
140
  });
104
141
  }
105
142
 
106
- async function _enrichWithLsp(result, lspConn) {
143
+ async function _enrichWithLsp(result, lspConn, workspace) {
107
144
  try {
108
145
  if (!Array.isArray(result?.content)) return result;
109
146
  const textBlock = result.content.find(c => c.type === "text");
@@ -112,7 +149,7 @@ async function _enrichWithLsp(result, lspConn) {
112
149
  try { parsed = JSON.parse(textBlock.text); } catch { return result; }
113
150
  if (!parsed.ok) return result;
114
151
  const paths = new Set();
115
- _extractPaths(parsed, paths);
152
+ _extractPaths(parsed, paths, workspace);
116
153
  if (paths.size === 0) return result;
117
154
  const diag = await _callLspHandler(lspConn.port, lspConn.secret, "diagnostics_recent", { paths: [...paths] });
118
155
  if (!diag?.ok || !diag.files || diag.total === 0) return result;
@@ -911,7 +948,7 @@ async function createBridgeServer(options) {
911
948
  const memoryTools = await listMemoryTools(memoryClient);
912
949
  const tools = dedupeTools([...indexerTools, ...memoryTools]);
913
950
 
914
- const lspAvailable = !!_readLspConnection();
951
+ const lspAvailable = !!_readLspConnection(workspace);
915
952
  if (lspAvailable) {
916
953
  const lspProp = {
917
954
  type: "boolean",
@@ -1052,7 +1089,7 @@ async function createBridgeServer(options) {
1052
1089
  }
1053
1090
 
1054
1091
  if (name && name.toLowerCase().startsWith("lsp_")) {
1055
- const lspConn = _readLspConnection();
1092
+ const lspConn = _readLspConnection(workspace);
1056
1093
  const lspPort = lspConn ? lspConn.port : null;
1057
1094
  const lspSecret = lspConn ? lspConn.secret : "";
1058
1095
  if (!lspPort) {
@@ -1064,7 +1101,7 @@ async function createBridgeServer(options) {
1064
1101
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Unknown LSP operation" }) }] };
1065
1102
  }
1066
1103
  try {
1067
- const lspArgs = params.arguments || {};
1104
+ const lspArgs = args || {};
1068
1105
  const postData = JSON.stringify(lspArgs);
1069
1106
  const result = await new Promise((resolve, reject) => {
1070
1107
  const req = http.request({
@@ -1078,6 +1115,7 @@ async function createBridgeServer(options) {
1078
1115
  let data = "";
1079
1116
  let exceeded = false;
1080
1117
  const MAX_RESP = 5 * 1024 * 1024;
1118
+ res.on("error", () => {});
1081
1119
  res.on("data", chunk => {
1082
1120
  if (exceeded) return;
1083
1121
  data += chunk;
@@ -1129,8 +1167,8 @@ async function createBridgeServer(options) {
1129
1167
  { timeout: timeoutMs },
1130
1168
  );
1131
1169
  let finalResult = maybeRemapToolResult(name, result, workspace);
1132
- const lspConn = includeLsp && _readLspConnection();
1133
- if (lspConn) finalResult = await _enrichWithLsp(finalResult, lspConn);
1170
+ const lspConn = includeLsp && _readLspConnection(workspace);
1171
+ if (lspConn) finalResult = await _enrichWithLsp(finalResult, lspConn, workspace);
1134
1172
  return finalResult;
1135
1173
  } catch (err) {
1136
1174
  lastError = err;