@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.
- package/package.json +1 -1
- package/src/mcpServer.js +147 -29
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,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
|
|
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
|
|
|
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
|
-
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|