@context-engine-bridge/context-engine-mcp-bridge 0.0.7 → 0.0.9
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/README.md +1 -1
- package/package.json +1 -1
- package/src/mcpServer.js +3 -1
- package/src/resultPathMapping.js +272 -39
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ other MCP clients) as long as the Context Engine stack is running.
|
|
|
15
15
|
## Prerequisites
|
|
16
16
|
|
|
17
17
|
- Node.js **>= 18** (see `engines` in `package.json`).
|
|
18
|
-
- A running Context Engine stack (e.g. via `docker-compose.
|
|
18
|
+
- A running Context Engine stack (e.g. via `docker-compose.yml`) with:
|
|
19
19
|
- MCP indexer HTTP endpoint (default: `http://localhost:8003/mcp`).
|
|
20
20
|
- MCP memory HTTP endpoint (optional, default: `http://localhost:8002/mcp`).
|
|
21
21
|
- For optional auth:
|
package/package.json
CHANGED
package/src/mcpServer.js
CHANGED
|
@@ -10,7 +10,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
10
10
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
11
11
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
12
12
|
import { loadAnyAuthEntry, loadAuthEntry } from "./authConfig.js";
|
|
13
|
-
import { maybeRemapToolResult } from "./resultPathMapping.js";
|
|
13
|
+
import { maybeRemapToolArgs, maybeRemapToolResult } from "./resultPathMapping.js";
|
|
14
14
|
|
|
15
15
|
function debugLog(message) {
|
|
16
16
|
try {
|
|
@@ -503,6 +503,8 @@ async function createBridgeServer(options) {
|
|
|
503
503
|
args = obj;
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
args = maybeRemapToolArgs(name, args, workspace);
|
|
507
|
+
|
|
506
508
|
if (name === "set_session_defaults") {
|
|
507
509
|
const indexerResult = await indexerClient.callTool({ name, arguments: args });
|
|
508
510
|
if (memoryClient) {
|
package/src/resultPathMapping.js
CHANGED
|
@@ -28,18 +28,170 @@ function _posixToNative(rel) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function
|
|
31
|
+
function _nativeToPosix(p) {
|
|
32
32
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
if (!p) {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
return String(p).split(path.sep).join("/");
|
|
37
|
+
} catch {
|
|
38
|
+
return p;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _workPathToRepoRelPosix(p) {
|
|
43
|
+
try {
|
|
44
|
+
const s = typeof p === "string" ? p.trim() : "";
|
|
45
|
+
if (!s || !s.startsWith("/work/")) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const rest = s.slice("/work/".length);
|
|
49
|
+
const parts = rest.split("/").filter(Boolean);
|
|
50
|
+
if (parts.length >= 2) {
|
|
51
|
+
return parts.slice(1).join("/");
|
|
52
|
+
}
|
|
53
|
+
if (parts.length === 1) {
|
|
54
|
+
return parts[0];
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeToolArgPath(p, workspaceRoot) {
|
|
63
|
+
try {
|
|
64
|
+
const s = typeof p === "string" ? p.trim() : "";
|
|
65
|
+
if (!s) {
|
|
66
|
+
return p;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const root = typeof workspaceRoot === "string" ? workspaceRoot : "";
|
|
70
|
+
const sPosix = s.replace(/\\/g, "/");
|
|
71
|
+
|
|
72
|
+
const fromWork = _workPathToRepoRelPosix(sPosix);
|
|
73
|
+
if (typeof fromWork === "string" && fromWork) {
|
|
74
|
+
return fromWork;
|
|
75
|
+
}
|
|
76
|
+
if (fromWork === "") {
|
|
77
|
+
return p;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (root) {
|
|
81
|
+
try {
|
|
82
|
+
const sNorm = s.replace(/\\/g, path.sep);
|
|
83
|
+
const rootNorm = root.replace(/\\/g, path.sep);
|
|
84
|
+
if (sNorm === rootNorm || sNorm.startsWith(rootNorm + path.sep)) {
|
|
85
|
+
const relNative = path.relative(rootNorm, sNorm);
|
|
86
|
+
const relPosix = _nativeToPosix(relNative);
|
|
87
|
+
if (relPosix && relPosix !== "." && relPosix !== ".." && !relPosix.startsWith("../")) {
|
|
88
|
+
return relPosix;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// ignore
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const base = path.posix.basename(root.replace(/\\/g, "/"));
|
|
96
|
+
if (base && sPosix.startsWith(base + "/")) {
|
|
97
|
+
const rest = sPosix.slice((base + "/").length);
|
|
98
|
+
if (rest && rest !== "." && rest !== ".." && !rest.startsWith("../")) {
|
|
99
|
+
return rest;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// ignore
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (sPosix.startsWith("./")) {
|
|
108
|
+
const rest = sPosix.slice(2);
|
|
109
|
+
if (rest && rest !== "." && rest !== ".." && !rest.startsWith("../")) {
|
|
110
|
+
return rest;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (sPosix === ".") {
|
|
114
|
+
return "";
|
|
115
|
+
}
|
|
116
|
+
return p;
|
|
117
|
+
} catch {
|
|
118
|
+
return p;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeToolArgGlob(p, workspaceRoot) {
|
|
123
|
+
try {
|
|
124
|
+
const s = typeof p === "string" ? p : "";
|
|
125
|
+
if (!s) {
|
|
126
|
+
return p;
|
|
127
|
+
}
|
|
128
|
+
// TODO(ctxce): If this becomes annoying, consider making glob normalization
|
|
129
|
+
// more conservative (e.g. only strip a repo prefix when followed by "/",
|
|
130
|
+
// and avoid collapsing "<repo>/**" into "**" which can broaden scope).
|
|
131
|
+
if (s.startsWith("!")) {
|
|
132
|
+
const rest = s.slice(1);
|
|
133
|
+
const mapped = normalizeToolArgPath(rest, workspaceRoot);
|
|
134
|
+
if (typeof mapped === "string") {
|
|
135
|
+
return "!" + mapped;
|
|
136
|
+
}
|
|
137
|
+
return p;
|
|
138
|
+
}
|
|
139
|
+
return normalizeToolArgPath(s, workspaceRoot);
|
|
140
|
+
} catch {
|
|
141
|
+
return p;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function applyPathMappingToArgs(value, workspaceRoot, keyHint = "") {
|
|
146
|
+
try {
|
|
147
|
+
if (value === null || value === undefined) {
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const key = typeof keyHint === "string" ? keyHint : "";
|
|
152
|
+
const lowered = key.toLowerCase();
|
|
153
|
+
const shouldMapString =
|
|
154
|
+
lowered === "path" ||
|
|
155
|
+
lowered === "under" ||
|
|
156
|
+
lowered === "root" ||
|
|
157
|
+
lowered === "subdir" ||
|
|
158
|
+
lowered === "path_glob" ||
|
|
159
|
+
lowered === "not_glob";
|
|
160
|
+
|
|
161
|
+
if (typeof value === "string") {
|
|
162
|
+
if (!shouldMapString) {
|
|
163
|
+
return value;
|
|
39
164
|
}
|
|
40
|
-
if (
|
|
41
|
-
return
|
|
165
|
+
if (lowered === "path_glob" || lowered === "not_glob") {
|
|
166
|
+
return normalizeToolArgGlob(value, workspaceRoot);
|
|
167
|
+
}
|
|
168
|
+
return normalizeToolArgPath(value, workspaceRoot);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
return value.map((v) => applyPathMappingToArgs(v, workspaceRoot, keyHint));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (typeof value === "object") {
|
|
176
|
+
const out = { ...value };
|
|
177
|
+
for (const [k, v] of Object.entries(out)) {
|
|
178
|
+
out[k] = applyPathMappingToArgs(v, workspaceRoot, k);
|
|
42
179
|
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return value;
|
|
184
|
+
} catch {
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function computeWorkspaceRelativePath(containerPath, hostPath) {
|
|
190
|
+
try {
|
|
191
|
+
const cont = typeof containerPath === "string" ? containerPath.trim() : "";
|
|
192
|
+
const rel = _workPathToRepoRelPosix(cont);
|
|
193
|
+
if (typeof rel === "string" && rel) {
|
|
194
|
+
return rel;
|
|
43
195
|
}
|
|
44
196
|
} catch {
|
|
45
197
|
}
|
|
@@ -55,17 +207,70 @@ function computeWorkspaceRelativePath(containerPath, hostPath) {
|
|
|
55
207
|
}
|
|
56
208
|
}
|
|
57
209
|
|
|
210
|
+
function remapRelatedPathToClient(p, workspaceRoot) {
|
|
211
|
+
try {
|
|
212
|
+
const s = typeof p === "string" ? p : "";
|
|
213
|
+
const root = typeof workspaceRoot === "string" ? workspaceRoot : "";
|
|
214
|
+
if (!s || !root) {
|
|
215
|
+
return p;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const sNorm = s.replace(/\\/g, path.sep);
|
|
219
|
+
if (sNorm.startsWith(root + path.sep) || sNorm === root) {
|
|
220
|
+
return sNorm;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const rel = _workPathToRepoRelPosix(s);
|
|
224
|
+
if (typeof rel === "string" && rel) {
|
|
225
|
+
const relNative = _posixToNative(rel);
|
|
226
|
+
return path.join(root, relNative);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// If it's already a relative path, join it to the workspace root.
|
|
230
|
+
if (!s.startsWith("/") && !s.includes(":") && !s.includes("\\")) {
|
|
231
|
+
const relPosix = s.trim();
|
|
232
|
+
if (relPosix && relPosix !== "." && !relPosix.startsWith("../") && relPosix !== "..") {
|
|
233
|
+
const relNative = _posixToNative(relPosix);
|
|
234
|
+
const joined = path.join(root, relNative);
|
|
235
|
+
const relCheck = path.relative(root, joined);
|
|
236
|
+
if (relCheck && !relCheck.startsWith(`..${path.sep}`) && relCheck !== "..") {
|
|
237
|
+
return joined;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return p;
|
|
243
|
+
} catch {
|
|
244
|
+
return p;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
58
248
|
function remapHitPaths(hit, workspaceRoot) {
|
|
59
249
|
if (!hit || typeof hit !== "object") {
|
|
60
250
|
return hit;
|
|
61
251
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
252
|
+
const rawPath = typeof hit.path === "string" ? hit.path : "";
|
|
253
|
+
let hostPath = typeof hit.host_path === "string" ? hit.host_path : "";
|
|
254
|
+
let containerPath = typeof hit.container_path === "string" ? hit.container_path : "";
|
|
255
|
+
if (!hostPath && rawPath) {
|
|
256
|
+
hostPath = rawPath;
|
|
257
|
+
}
|
|
258
|
+
if (!containerPath && rawPath) {
|
|
259
|
+
containerPath = rawPath;
|
|
260
|
+
}
|
|
64
261
|
const relPath = computeWorkspaceRelativePath(containerPath, hostPath);
|
|
65
262
|
const out = { ...hit };
|
|
66
263
|
if (relPath) {
|
|
67
264
|
out.rel_path = relPath;
|
|
68
265
|
}
|
|
266
|
+
// Remap related_paths nested under each hit (repo_search/hybrid_search emit this per result).
|
|
267
|
+
try {
|
|
268
|
+
if (Array.isArray(out.related_paths)) {
|
|
269
|
+
out.related_paths = out.related_paths.map((p) => remapRelatedPathToClient(p, workspaceRoot));
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
// ignore
|
|
273
|
+
}
|
|
69
274
|
if (workspaceRoot && relPath) {
|
|
70
275
|
try {
|
|
71
276
|
const relNative = _posixToNative(relPath);
|
|
@@ -107,29 +312,49 @@ function remapHitPaths(hit, workspaceRoot) {
|
|
|
107
312
|
}
|
|
108
313
|
}
|
|
109
314
|
const overridePath = envTruthy(process.env.CTXCE_BRIDGE_OVERRIDE_PATH, true);
|
|
110
|
-
if (overridePath
|
|
111
|
-
out.
|
|
315
|
+
if (overridePath) {
|
|
316
|
+
if (typeof out.client_path === "string" && out.client_path) {
|
|
317
|
+
out.path = out.client_path;
|
|
318
|
+
} else if (relPath) {
|
|
319
|
+
out.path = relPath;
|
|
320
|
+
}
|
|
112
321
|
}
|
|
113
322
|
return out;
|
|
114
323
|
}
|
|
115
324
|
|
|
116
|
-
function remapStringPath(p) {
|
|
325
|
+
function remapStringPath(p, workspaceRoot) {
|
|
117
326
|
try {
|
|
118
327
|
const s = typeof p === "string" ? p : "";
|
|
119
328
|
if (!s) {
|
|
120
329
|
return p;
|
|
121
330
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
|
|
331
|
+
// If this is already a path within the current client workspace, rewrite to a
|
|
332
|
+
// workspace-relative string when override is enabled.
|
|
333
|
+
try {
|
|
334
|
+
const root = typeof workspaceRoot === "string" ? workspaceRoot : "";
|
|
335
|
+
if (root) {
|
|
336
|
+
const sNorm = s.replace(/\\/g, path.sep);
|
|
337
|
+
if (sNorm.startsWith(root + path.sep) || sNorm === root) {
|
|
338
|
+
const relNative = path.relative(root, sNorm);
|
|
339
|
+
const relPosix = String(relNative).split(path.sep).join("/");
|
|
340
|
+
if (relPosix && !relPosix.startsWith("../") && relPosix !== "..") {
|
|
341
|
+
const override = envTruthy(process.env.CTXCE_BRIDGE_OVERRIDE_PATH, true);
|
|
342
|
+
if (override) {
|
|
343
|
+
return relPosix;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
130
346
|
}
|
|
131
|
-
return p;
|
|
132
347
|
}
|
|
348
|
+
} catch {
|
|
349
|
+
// ignore
|
|
350
|
+
}
|
|
351
|
+
const rel = _workPathToRepoRelPosix(s);
|
|
352
|
+
if (typeof rel === "string" && rel) {
|
|
353
|
+
const override = envTruthy(process.env.CTXCE_BRIDGE_OVERRIDE_PATH, true);
|
|
354
|
+
if (override) {
|
|
355
|
+
return rel;
|
|
356
|
+
}
|
|
357
|
+
return p;
|
|
133
358
|
}
|
|
134
359
|
return p;
|
|
135
360
|
} catch {
|
|
@@ -191,18 +416,7 @@ function applyPathMappingToPayload(payload, workspaceRoot) {
|
|
|
191
416
|
out.citations = mapHitsArray(out.citations);
|
|
192
417
|
}
|
|
193
418
|
if (Array.isArray(out.related_paths)) {
|
|
194
|
-
out.related_paths = out.related_paths.map((p) =>
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// context_search: {results:[{source:"code"|"memory", ...}]}
|
|
198
|
-
if (Array.isArray(out.results)) {
|
|
199
|
-
out.results = out.results.map((r) => {
|
|
200
|
-
if (!r || typeof r !== "object") {
|
|
201
|
-
return r;
|
|
202
|
-
}
|
|
203
|
-
// Only code results have path-like fields
|
|
204
|
-
return remapHitPaths(r, workspaceRoot);
|
|
205
|
-
});
|
|
419
|
+
out.related_paths = out.related_paths.map((p) => remapRelatedPathToClient(p, workspaceRoot));
|
|
206
420
|
}
|
|
207
421
|
|
|
208
422
|
// Some tools nest under {result:{...}}
|
|
@@ -242,25 +456,44 @@ export function maybeRemapToolResult(name, result, workspaceRoot) {
|
|
|
242
456
|
}
|
|
243
457
|
|
|
244
458
|
const mapped = applyPathMappingToPayload(parsed.value, workspaceRoot);
|
|
459
|
+
let outResult = result;
|
|
245
460
|
if (parsed.mode === "structured") {
|
|
246
|
-
|
|
461
|
+
outResult = { ...result, structuredContent: mapped };
|
|
247
462
|
}
|
|
248
463
|
|
|
249
464
|
// Replace text payload for clients that only read `content[].text`
|
|
250
465
|
try {
|
|
251
|
-
const content = Array.isArray(
|
|
466
|
+
const content = Array.isArray(outResult.content) ? outResult.content.slice() : [];
|
|
252
467
|
const idx = content.findIndex(
|
|
253
468
|
(c) => c && c.type === "text" && typeof c.text === "string",
|
|
254
469
|
);
|
|
255
470
|
if (idx >= 0) {
|
|
256
471
|
content[idx] = { ...content[idx], text: JSON.stringify(mapped) };
|
|
257
|
-
|
|
472
|
+
outResult = { ...outResult, content };
|
|
258
473
|
}
|
|
259
474
|
} catch {
|
|
260
475
|
// ignore
|
|
261
476
|
}
|
|
262
|
-
return
|
|
477
|
+
return outResult;
|
|
263
478
|
} catch {
|
|
264
479
|
return result;
|
|
265
480
|
}
|
|
266
481
|
}
|
|
482
|
+
|
|
483
|
+
export function maybeRemapToolArgs(name, args, workspaceRoot) {
|
|
484
|
+
try {
|
|
485
|
+
if (!name || !workspaceRoot) {
|
|
486
|
+
return args;
|
|
487
|
+
}
|
|
488
|
+
const enabled = envTruthy(process.env.CTXCE_BRIDGE_MAP_ARGS, true);
|
|
489
|
+
if (!enabled) {
|
|
490
|
+
return args;
|
|
491
|
+
}
|
|
492
|
+
if (args === null || args === undefined || typeof args !== "object") {
|
|
493
|
+
return args;
|
|
494
|
+
}
|
|
495
|
+
return applyPathMappingToArgs(args, workspaceRoot, "");
|
|
496
|
+
} catch {
|
|
497
|
+
return args;
|
|
498
|
+
}
|
|
499
|
+
}
|