@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.
- package/package.json +1 -1
- package/src/mcpServer.js +72 -34
package/package.json
CHANGED
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
|
|
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))
|
|
47
|
+
if (envPort && _isValidPort(envPort)) {
|
|
48
|
+
return { port: envPort, secret: process.env.CTXCE_LSP_SECRET || "" };
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
const now = Date.now();
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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" &&
|
|
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 =
|
|
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;
|