@context-engine-bridge/context-engine-mcp-bridge 0.0.32 → 0.0.34
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 +172 -14
package/package.json
CHANGED
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;
|
|
@@ -74,16 +102,41 @@ const _LSP_ENRICHABLE_TOOLS = new Set([
|
|
|
74
102
|
"search_config_for", "search_callers_for", "search_importers_for",
|
|
75
103
|
]);
|
|
76
104
|
|
|
77
|
-
function
|
|
105
|
+
function _isAbsolutePath(p) {
|
|
106
|
+
return p.startsWith("/") || /^[a-zA-Z]:[/\\]/.test(p);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _resolveAndContain(relPath, workspace) {
|
|
110
|
+
const resolved = path.resolve(workspace, relPath);
|
|
111
|
+
if (resolved === workspace || resolved.startsWith(workspace + path.sep)) return resolved;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _extractPaths(obj, paths, workspace, depth = 0) {
|
|
78
116
|
if (!obj || typeof obj !== "object" || depth > 20) return;
|
|
79
117
|
if (Array.isArray(obj)) {
|
|
80
|
-
for (const item of obj) _extractPaths(item, paths, depth + 1);
|
|
118
|
+
for (const item of obj) _extractPaths(item, paths, workspace, depth + 1);
|
|
81
119
|
return;
|
|
82
120
|
}
|
|
83
|
-
if (typeof obj.path === "string" &&
|
|
121
|
+
if (typeof obj.path === "string" && obj.path.length > 0) {
|
|
122
|
+
if (_isAbsolutePath(obj.path)) {
|
|
123
|
+
if (workspace) {
|
|
124
|
+
const contained = _resolveAndContain(obj.path, workspace);
|
|
125
|
+
if (contained) paths.add(contained);
|
|
126
|
+
} else {
|
|
127
|
+
paths.add(obj.path);
|
|
128
|
+
}
|
|
129
|
+
} else if (workspace) {
|
|
130
|
+
const contained = _resolveAndContain(obj.path, workspace);
|
|
131
|
+
if (contained) paths.add(contained);
|
|
132
|
+
}
|
|
133
|
+
} else if (typeof obj.rel_path === "string" && obj.rel_path.length > 0 && workspace) {
|
|
134
|
+
const contained = _resolveAndContain(obj.rel_path, workspace);
|
|
135
|
+
if (contained) paths.add(contained);
|
|
136
|
+
}
|
|
84
137
|
for (const [key, val] of Object.entries(obj)) {
|
|
85
138
|
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
86
|
-
if (val && typeof val === "object") _extractPaths(val, paths, depth + 1);
|
|
139
|
+
if (val && typeof val === "object") _extractPaths(val, paths, workspace, depth + 1);
|
|
87
140
|
}
|
|
88
141
|
}
|
|
89
142
|
|
|
@@ -105,6 +158,7 @@ function _callLspHandler(port, secret, operation, params) {
|
|
|
105
158
|
let data = "";
|
|
106
159
|
let exceeded = false;
|
|
107
160
|
const MAX_RESP = 5 * 1024 * 1024;
|
|
161
|
+
res.on("error", () => {});
|
|
108
162
|
res.on("data", chunk => {
|
|
109
163
|
if (exceeded) return;
|
|
110
164
|
data += chunk;
|
|
@@ -119,7 +173,7 @@ function _callLspHandler(port, secret, operation, params) {
|
|
|
119
173
|
});
|
|
120
174
|
}
|
|
121
175
|
|
|
122
|
-
async function _enrichWithLsp(result, lspConn) {
|
|
176
|
+
async function _enrichWithLsp(result, lspConn, workspace) {
|
|
123
177
|
try {
|
|
124
178
|
if (!Array.isArray(result?.content)) return result;
|
|
125
179
|
const textBlock = result.content.find(c => c.type === "text");
|
|
@@ -127,16 +181,34 @@ async function _enrichWithLsp(result, lspConn) {
|
|
|
127
181
|
let parsed;
|
|
128
182
|
try { parsed = JSON.parse(textBlock.text); } catch { return result; }
|
|
129
183
|
if (!parsed.ok) return result;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
184
|
+
if (_lspCircuitOpen()) {
|
|
185
|
+
parsed._lsp_status = "circuit_open";
|
|
186
|
+
} else {
|
|
187
|
+
const paths = new Set();
|
|
188
|
+
_extractPaths(parsed, paths, workspace);
|
|
189
|
+
if (paths.size === 0) {
|
|
190
|
+
parsed._lsp_status = "no_paths";
|
|
191
|
+
} else {
|
|
192
|
+
const diag = await _callLspHandler(lspConn.port, lspConn.secret, "diagnostics_recent", { paths: [...paths] });
|
|
193
|
+
if (!diag) {
|
|
194
|
+
_lspCircuitRecordFailure();
|
|
195
|
+
parsed._lsp_status = "connection_failed";
|
|
196
|
+
} else {
|
|
197
|
+
_lspCircuitRecordSuccess();
|
|
198
|
+
if (!diag.ok || !diag.files || diag.total === 0) {
|
|
199
|
+
parsed._lsp_status = "no_diagnostics";
|
|
200
|
+
} else {
|
|
201
|
+
parsed._lsp = { diagnostics: diag.files, total: diag.total };
|
|
202
|
+
parsed._lsp_status = "ok";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
136
207
|
textBlock.text = JSON.stringify(parsed);
|
|
137
208
|
return result;
|
|
138
209
|
} catch (err) {
|
|
139
210
|
debugLog("[ctxce] LSP enrichment failed: " + String(err));
|
|
211
|
+
_lspCircuitRecordFailure();
|
|
140
212
|
return result;
|
|
141
213
|
}
|
|
142
214
|
}
|
|
@@ -565,8 +637,88 @@ async function fetchBridgeCollectionState({
|
|
|
565
637
|
}
|
|
566
638
|
}
|
|
567
639
|
|
|
640
|
+
function _validateWorkspacePath(raw) {
|
|
641
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
642
|
+
const resolved = path.resolve(raw);
|
|
643
|
+
if (!_isAbsolutePath(resolved)) return null;
|
|
644
|
+
try {
|
|
645
|
+
const stat = fs.statSync(resolved);
|
|
646
|
+
if (!stat.isDirectory()) return null;
|
|
647
|
+
} catch (_) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
return resolved;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const MAX_WS_SCAN = 50;
|
|
654
|
+
|
|
655
|
+
function _resolveWorkspace(providedWorkspace) {
|
|
656
|
+
const wsDir = _computeWorkspaceDir(providedWorkspace);
|
|
657
|
+
const now = Date.now();
|
|
658
|
+
const connResult = _tryReadConnFile(path.join(wsDir, "lsp-connection.json"), now);
|
|
659
|
+
if (connResult) return providedWorkspace;
|
|
660
|
+
|
|
661
|
+
const wsRoot = path.join(os.homedir(), ".ctxce", "workspaces");
|
|
662
|
+
try {
|
|
663
|
+
const dirs = fs.readdirSync(wsRoot).slice(0, MAX_WS_SCAN);
|
|
664
|
+
let bestLspMatch = null;
|
|
665
|
+
let bestLspTs = 0;
|
|
666
|
+
let bestMetaMatch = null;
|
|
667
|
+
let bestMetaTs = 0;
|
|
668
|
+
for (const dir of dirs) {
|
|
669
|
+
if (dir === "." || dir === ".." || dir.includes("/") || dir.includes("\0")) continue;
|
|
670
|
+
const dirPath = path.join(wsRoot, dir);
|
|
671
|
+
try {
|
|
672
|
+
const lstat = fs.lstatSync(dirPath);
|
|
673
|
+
if (!lstat.isDirectory()) continue;
|
|
674
|
+
} catch (_) { continue; }
|
|
675
|
+
try {
|
|
676
|
+
const meta = JSON.parse(fs.readFileSync(path.join(dirPath, "meta.json"), "utf8"));
|
|
677
|
+
const wp = _validateWorkspacePath(meta.workspace_path);
|
|
678
|
+
if (!wp) continue;
|
|
679
|
+
const connPath = path.join(dirPath, "lsp-connection.json");
|
|
680
|
+
try {
|
|
681
|
+
const conn = JSON.parse(fs.readFileSync(connPath, "utf8"));
|
|
682
|
+
if (conn.pid && conn.port && _isValidPort(conn.port)) {
|
|
683
|
+
try {
|
|
684
|
+
process.kill(conn.pid, 0);
|
|
685
|
+
if (conn.created_at && (now - conn.created_at) > LSP_CONN_MAX_AGE) {
|
|
686
|
+
} else {
|
|
687
|
+
const connTs = typeof conn.created_at === "number" ? conn.created_at : 0;
|
|
688
|
+
if (connTs > bestLspTs) {
|
|
689
|
+
bestLspTs = connTs;
|
|
690
|
+
bestLspMatch = wp;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} catch (_) {}
|
|
694
|
+
}
|
|
695
|
+
} catch (_) {}
|
|
696
|
+
const updatedAt = typeof meta.updated_at === "number"
|
|
697
|
+
? meta.updated_at
|
|
698
|
+
: typeof meta.updated_at === "string"
|
|
699
|
+
? new Date(meta.updated_at).getTime() || 0
|
|
700
|
+
: 0;
|
|
701
|
+
if (updatedAt > bestMetaTs) {
|
|
702
|
+
bestMetaTs = updatedAt;
|
|
703
|
+
bestMetaMatch = wp;
|
|
704
|
+
}
|
|
705
|
+
} catch (_) {}
|
|
706
|
+
}
|
|
707
|
+
if (bestLspMatch) {
|
|
708
|
+
debugLog("[ctxce] Resolved workspace from active LSP connection");
|
|
709
|
+
return bestLspMatch;
|
|
710
|
+
}
|
|
711
|
+
if (bestMetaMatch) {
|
|
712
|
+
debugLog("[ctxce] Resolved workspace from most recent meta.json");
|
|
713
|
+
return bestMetaMatch;
|
|
714
|
+
}
|
|
715
|
+
} catch (_) {}
|
|
716
|
+
|
|
717
|
+
return providedWorkspace;
|
|
718
|
+
}
|
|
719
|
+
|
|
568
720
|
async function createBridgeServer(options) {
|
|
569
|
-
const workspace = options.workspace || process.cwd();
|
|
721
|
+
const workspace = _resolveWorkspace(options.workspace || process.cwd());
|
|
570
722
|
const indexerUrl = options.indexerUrl;
|
|
571
723
|
const memoryUrl = options.memoryUrl;
|
|
572
724
|
const explicitCollection = options.collection;
|
|
@@ -1068,6 +1220,9 @@ async function createBridgeServer(options) {
|
|
|
1068
1220
|
}
|
|
1069
1221
|
|
|
1070
1222
|
if (name && name.toLowerCase().startsWith("lsp_")) {
|
|
1223
|
+
if (_lspCircuitOpen()) {
|
|
1224
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "LSP proxy temporarily unavailable (circuit breaker open after repeated failures). Will retry automatically." }) }] };
|
|
1225
|
+
}
|
|
1071
1226
|
const lspConn = _readLspConnection(workspace);
|
|
1072
1227
|
const lspPort = lspConn ? lspConn.port : null;
|
|
1073
1228
|
const lspSecret = lspConn ? lspConn.secret : "";
|
|
@@ -1089,11 +1244,12 @@ async function createBridgeServer(options) {
|
|
|
1089
1244
|
path: `/lsp/${operation}`,
|
|
1090
1245
|
method: "POST",
|
|
1091
1246
|
headers: Object.assign({ "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData) }, lspSecret ? { "x-lsp-handler-token": lspSecret } : {}),
|
|
1092
|
-
timeout:
|
|
1247
|
+
timeout: LSP_DIRECT_TIMEOUT_MS,
|
|
1093
1248
|
}, (res) => {
|
|
1094
1249
|
let data = "";
|
|
1095
1250
|
let exceeded = false;
|
|
1096
1251
|
const MAX_RESP = 5 * 1024 * 1024;
|
|
1252
|
+
res.on("error", () => {});
|
|
1097
1253
|
res.on("data", chunk => {
|
|
1098
1254
|
if (exceeded) return;
|
|
1099
1255
|
data += chunk;
|
|
@@ -1108,8 +1264,10 @@ async function createBridgeServer(options) {
|
|
|
1108
1264
|
req.write(postData);
|
|
1109
1265
|
req.end();
|
|
1110
1266
|
});
|
|
1267
|
+
_lspCircuitRecordSuccess();
|
|
1111
1268
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1112
1269
|
} catch (err) {
|
|
1270
|
+
_lspCircuitRecordFailure();
|
|
1113
1271
|
debugLog("[ctxce] LSP proxy error: " + String(err));
|
|
1114
1272
|
const safeMsg = (err && err.code) ? `LSP proxy error: ${err.code}` : "LSP proxy error: request failed";
|
|
1115
1273
|
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: safeMsg }) }] };
|
|
@@ -1146,7 +1304,7 @@ async function createBridgeServer(options) {
|
|
|
1146
1304
|
);
|
|
1147
1305
|
let finalResult = maybeRemapToolResult(name, result, workspace);
|
|
1148
1306
|
const lspConn = includeLsp && _readLspConnection(workspace);
|
|
1149
|
-
if (lspConn) finalResult = await _enrichWithLsp(finalResult, lspConn);
|
|
1307
|
+
if (lspConn) finalResult = await _enrichWithLsp(finalResult, lspConn, workspace);
|
|
1150
1308
|
return finalResult;
|
|
1151
1309
|
} catch (err) {
|
|
1152
1310
|
lastError = err;
|