@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 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.dev-remote.yml`) with:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
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
@@ -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) {
@@ -28,18 +28,170 @@ function _posixToNative(rel) {
28
28
  }
29
29
  }
30
30
 
31
- function computeWorkspaceRelativePath(containerPath, hostPath) {
31
+ function _nativeToPosix(p) {
32
32
  try {
33
- const cont = typeof containerPath === "string" ? containerPath.trim() : "";
34
- if (cont.startsWith("/work/")) {
35
- const rest = cont.slice("/work/".length);
36
- const parts = rest.split("/").filter(Boolean);
37
- if (parts.length >= 2) {
38
- return parts.slice(1).join("/");
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 (parts.length === 1) {
41
- return parts[0];
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 hostPath = typeof hit.host_path === "string" ? hit.host_path : "";
63
- const containerPath = typeof hit.container_path === "string" ? hit.container_path : "";
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 && relPath) {
111
- out.path = relPath;
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
- if (s.startsWith("/work/")) {
123
- const rest = s.slice("/work/".length);
124
- const parts = rest.split("/").filter(Boolean);
125
- if (parts.length >= 2) {
126
- const rel = parts.slice(1).join("/");
127
- const override = envTruthy(process.env.CTXCE_BRIDGE_OVERRIDE_PATH, true);
128
- if (override) {
129
- return rel;
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) => remapStringPath(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
- return { ...result, structuredContent: mapped };
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(result.content) ? result.content.slice() : [];
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
- return { ...result, content };
472
+ outResult = { ...outResult, content };
258
473
  }
259
474
  } catch {
260
475
  // ignore
261
476
  }
262
- return result;
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
+ }