@context-engine-bridge/context-engine-mcp-bridge 0.0.6 → 0.0.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
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/authCli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import process from "node:process";
2
- import { loadAuthEntry, saveAuthEntry, deleteAuthEntry } from "./authConfig.js";
2
+ import { loadAuthEntry, saveAuthEntry, deleteAuthEntry, loadAnyAuthEntry } from "./authConfig.js";
3
3
 
4
4
  function parseAuthArgs(args) {
5
5
  let backendUrl = process.env.CTXCE_AUTH_BACKEND_URL || "";
@@ -19,9 +19,16 @@ function parseAuthArgs(args) {
19
19
  i += 1;
20
20
  continue;
21
21
  }
22
- if ((a === "--username" || a === "--user") && i + 1 < args.length) {
23
- username = args[i + 1];
24
- i += 1;
22
+ if (a === "--username" || a === "--user") {
23
+ const hasNext = i + 1 < args.length;
24
+ const next = hasNext ? String(args[i + 1]) : "";
25
+ if (hasNext && !next.startsWith("-")) {
26
+ username = args[i + 1];
27
+ i += 1;
28
+ } else {
29
+ console.error("[ctxce] Missing value for --username/--user; expected a username.");
30
+ process.exit(1);
31
+ }
25
32
  continue;
26
33
  }
27
34
  if ((a === "--password" || a === "--pass") && i + 1 < args.length) {
@@ -41,6 +48,11 @@ function getBackendUrl(backendUrl) {
41
48
  return (backendUrl || process.env.CTXCE_AUTH_BACKEND_URL || "").trim();
42
49
  }
43
50
 
51
+ function getDefaultUploadBackend() {
52
+ // Default to upload service when nothing else is configured
53
+ return (process.env.CTXCE_UPLOAD_ENDPOINT || process.env.UPLOAD_ENDPOINT || "http://localhost:8004").trim();
54
+ }
55
+
44
56
  function requireBackendUrl(backendUrl) {
45
57
  const url = getBackendUrl(backendUrl);
46
58
  if (!url) {
@@ -67,7 +79,26 @@ function outputJsonStatus(url, state, entry, rawExpires) {
67
79
 
68
80
  async function doLogin(args) {
69
81
  const { backendUrl, token, username, password } = parseAuthArgs(args);
70
- const url = requireBackendUrl(backendUrl);
82
+ let url = getBackendUrl(backendUrl);
83
+ if (!url) {
84
+ // Fallback: use any stored auth entry when no backend is provided
85
+ const any = loadAnyAuthEntry();
86
+ if (any && any.backendUrl) {
87
+ url = any.backendUrl;
88
+ console.error("[ctxce] Using stored backend for login:", url);
89
+ }
90
+ }
91
+ if (!url) {
92
+ // Final fallback: default upload endpoint (extension's upload endpoint or localhost:8004)
93
+ url = getDefaultUploadBackend();
94
+ if (url) {
95
+ console.error("[ctxce] Using default upload backend for login:", url);
96
+ }
97
+ }
98
+ if (!url) {
99
+ console.error("[ctxce] Auth backend URL not configured. Set CTXCE_AUTH_BACKEND_URL or use --backend-url.");
100
+ process.exit(1);
101
+ }
71
102
  const trimmedUser = (username || "").trim();
72
103
  const usePassword = trimmedUser && (password || "").length > 0;
73
104
 
@@ -124,13 +155,30 @@ async function doLogin(args) {
124
155
 
125
156
  async function doStatus(args) {
126
157
  const { backendUrl, outputJson } = parseAuthArgs(args);
127
- const url = getBackendUrl(backendUrl);
158
+ let url = getBackendUrl(backendUrl);
159
+ let usedFallback = false;
160
+ if (!url) {
161
+ // Fallback: use any stored auth entry when no backend is provided
162
+ const any = loadAnyAuthEntry();
163
+ if (any && any.backendUrl) {
164
+ url = any.backendUrl;
165
+ usedFallback = true;
166
+ }
167
+ }
168
+ if (!url) {
169
+ // Final fallback: default upload endpoint
170
+ url = getDefaultUploadBackend();
171
+ if (url) {
172
+ usedFallback = true;
173
+ console.error("[ctxce] Using default upload backend for status:", url);
174
+ }
175
+ }
128
176
  if (!url) {
129
177
  if (outputJson) {
130
178
  outputJsonStatus("", "missing_backend", null, null);
131
179
  process.exit(1);
132
180
  }
133
- console.error("[ctxce] Auth backend URL not configured. Set CTXCE_AUTH_BACKEND_URL or use --backend-url.");
181
+ console.error("[ctxce] Auth backend URL not configured and no stored sessions found. Set CTXCE_AUTH_BACKEND_URL or use --backend-url.");
134
182
  process.exit(1);
135
183
  }
136
184
  let entry;
@@ -149,7 +197,11 @@ async function doStatus(args) {
149
197
  outputJsonStatus(url, "missing", null, rawExpires);
150
198
  process.exit(1);
151
199
  }
152
- console.error("[ctxce] Not logged in for", url);
200
+ if (usedFallback) {
201
+ console.error("[ctxce] Not logged in for stored backend", url);
202
+ } else {
203
+ console.error("[ctxce] Not logged in for", url);
204
+ }
153
205
  process.exit(1);
154
206
  }
155
207
 
@@ -158,7 +210,11 @@ async function doStatus(args) {
158
210
  outputJsonStatus(url, "expired", entry, rawExpires);
159
211
  process.exit(2);
160
212
  }
161
- console.error("[ctxce] Stored auth session appears expired for", url);
213
+ if (usedFallback) {
214
+ console.error("[ctxce] Stored auth session appears expired for stored backend", url);
215
+ } else {
216
+ console.error("[ctxce] Stored auth session appears expired for", url);
217
+ }
162
218
  if (rawExpires) {
163
219
  console.error("[ctxce] Session expired at", rawExpires);
164
220
  }
@@ -169,6 +225,9 @@ async function doStatus(args) {
169
225
  outputJsonStatus(url, "ok", entry, rawExpires);
170
226
  return;
171
227
  }
228
+ if (usedFallback) {
229
+ console.error("[ctxce] Using stored backend for status:", url);
230
+ }
172
231
  console.error("[ctxce] Logged in to", url, "as", entry.userId || "<unknown>");
173
232
  if (rawExpires) {
174
233
  console.error("[ctxce] Session expires at", rawExpires);
@@ -177,7 +236,26 @@ async function doStatus(args) {
177
236
 
178
237
  async function doLogout(args) {
179
238
  const { backendUrl } = parseAuthArgs(args);
180
- const url = requireBackendUrl(backendUrl);
239
+ let url = getBackendUrl(backendUrl);
240
+ if (!url) {
241
+ // Fallback: use any stored auth entry when no backend is provided
242
+ const any = loadAnyAuthEntry();
243
+ if (any && any.backendUrl) {
244
+ url = any.backendUrl;
245
+ console.error("[ctxce] Using stored backend for logout:", url);
246
+ }
247
+ }
248
+ if (!url) {
249
+ // Final fallback: default upload endpoint
250
+ url = getDefaultUploadBackend();
251
+ if (url) {
252
+ console.error("[ctxce] Using default upload backend for logout:", url);
253
+ }
254
+ }
255
+ if (!url) {
256
+ console.error("[ctxce] Auth backend URL not configured and no stored sessions found. Set CTXCE_AUTH_BACKEND_URL or use --backend-url.");
257
+ process.exit(1);
258
+ }
181
259
  const entry = loadAuthEntry(url);
182
260
  if (!entry) {
183
261
  console.error("[ctxce] No stored auth session for", url);
package/src/mcpServer.js CHANGED
@@ -1,3 +1,17 @@
1
+ import process from "node:process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { execSync } from "node:child_process";
5
+ import { createServer } from "node:http";
6
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
10
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
11
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
12
+ import { loadAnyAuthEntry, loadAuthEntry } from "./authConfig.js";
13
+ import { maybeRemapToolResult } from "./resultPathMapping.js";
14
+
1
15
  function debugLog(message) {
2
16
  try {
3
17
  const text = typeof message === "string" ? message : String(message);
@@ -236,19 +250,6 @@ function isTransientToolError(error) {
236
250
  // Acts as a low-level proxy for tools, forwarding tools/list and tools/call
237
251
  // to the remote qdrant-indexer MCP server while adding a local `ping` tool.
238
252
 
239
- import process from "node:process";
240
- import fs from "node:fs";
241
- import path from "node:path";
242
- import { execSync } from "node:child_process";
243
- import { createServer } from "node:http";
244
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
245
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
246
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
247
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
248
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
249
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
250
- import { loadAnyAuthEntry, loadAuthEntry } from "./authConfig.js";
251
-
252
253
  async function createBridgeServer(options) {
253
254
  const workspace = options.workspace || process.cwd();
254
255
  const indexerUrl = options.indexerUrl;
@@ -286,18 +287,20 @@ async function createBridgeServer(options) {
286
287
  const authBackendUrl = process.env.CTXCE_AUTH_BACKEND_URL || "";
287
288
  let sessionId = explicitSession;
288
289
 
289
- if (!sessionId) {
290
+ function resolveSessionId() {
291
+ const explicit = process.env.CTXCE_SESSION_ID || "";
292
+ if (explicit) {
293
+ return explicit;
294
+ }
290
295
  let backendToUse = authBackendUrl;
291
296
  let entry = null;
292
-
293
297
  if (backendToUse) {
294
298
  try {
295
299
  entry = loadAuthEntry(backendToUse);
296
- } catch (err) {
300
+ } catch {
297
301
  entry = null;
298
302
  }
299
303
  }
300
-
301
304
  if (!entry) {
302
305
  try {
303
306
  const any = loadAnyAuthEntry();
@@ -305,11 +308,10 @@ async function createBridgeServer(options) {
305
308
  backendToUse = any.backendUrl;
306
309
  entry = any.entry;
307
310
  }
308
- } catch (err) {
311
+ } catch {
309
312
  entry = null;
310
313
  }
311
314
  }
312
-
313
315
  if (entry) {
314
316
  let expired = false;
315
317
  const rawExpires = entry.expiresAt;
@@ -320,11 +322,17 @@ async function createBridgeServer(options) {
320
322
  }
321
323
  }
322
324
  if (!expired && typeof entry.sessionId === "string" && entry.sessionId) {
323
- sessionId = entry.sessionId;
324
- } else if (expired) {
325
+ return entry.sessionId;
326
+ }
327
+ if (expired) {
325
328
  debugLog("[ctxce] Stored auth session appears expired; please run `ctxce auth login` again.");
326
329
  }
327
330
  }
331
+ return "";
332
+ }
333
+
334
+ if (!sessionId) {
335
+ sessionId = resolveSessionId();
328
336
  }
329
337
 
330
338
  if (!sessionId) {
@@ -477,7 +485,16 @@ async function createBridgeServer(options) {
477
485
 
478
486
  debugLog(`[ctxce] tools/call: ${name || "<no-name>"}`);
479
487
 
480
- // Attach session id so the target server can apply per-session defaults.
488
+ // Refresh session before each call; re-init clients if session changes.
489
+ const freshSession = resolveSessionId() || sessionId;
490
+ if (freshSession && freshSession !== sessionId) {
491
+ sessionId = freshSession;
492
+ try {
493
+ await initializeRemoteClients(true);
494
+ } catch (err) {
495
+ debugLog("[ctxce] Failed to reinitialize clients after session refresh: " + String(err));
496
+ }
497
+ }
481
498
  if (sessionId && (args === undefined || args === null || typeof args === "object")) {
482
499
  const obj = args && typeof args === "object" ? { ...args } : {};
483
500
  if (!Object.prototype.hasOwnProperty.call(obj, "session")) {
@@ -525,7 +542,7 @@ async function createBridgeServer(options) {
525
542
  undefined,
526
543
  { timeout: timeoutMs },
527
544
  );
528
- return result;
545
+ return maybeRemapToolResult(name, result, workspace);
529
546
  } catch (err) {
530
547
  lastError = err;
531
548
 
@@ -0,0 +1,338 @@
1
+ import process from "node:process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ function envTruthy(value, defaultVal = false) {
6
+ try {
7
+ if (value === undefined || value === null) {
8
+ return defaultVal;
9
+ }
10
+ const s = String(value).trim().toLowerCase();
11
+ if (!s) {
12
+ return defaultVal;
13
+ }
14
+ return s === "1" || s === "true" || s === "yes" || s === "on";
15
+ } catch {
16
+ return defaultVal;
17
+ }
18
+ }
19
+
20
+ function _posixToNative(rel) {
21
+ try {
22
+ if (!rel) {
23
+ return "";
24
+ }
25
+ return String(rel).split("/").join(path.sep);
26
+ } catch {
27
+ return rel;
28
+ }
29
+ }
30
+
31
+ function computeWorkspaceRelativePath(containerPath, hostPath) {
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("/");
39
+ }
40
+ if (parts.length === 1) {
41
+ return parts[0];
42
+ }
43
+ }
44
+ } catch {
45
+ }
46
+ try {
47
+ const hp = typeof hostPath === "string" ? hostPath.trim() : "";
48
+ if (!hp) {
49
+ return "";
50
+ }
51
+ // If we don't have a container path, at least try to return a basename.
52
+ return path.posix.basename(hp.replace(/\\/g, "/"));
53
+ } catch {
54
+ return "";
55
+ }
56
+ }
57
+
58
+ function remapRelatedPathToClient(p, workspaceRoot) {
59
+ try {
60
+ const s = typeof p === "string" ? p : "";
61
+ const root = typeof workspaceRoot === "string" ? workspaceRoot : "";
62
+ if (!s || !root) {
63
+ return p;
64
+ }
65
+
66
+ const sNorm = s.replace(/\\/g, path.sep);
67
+ if (sNorm.startsWith(root + path.sep) || sNorm === root) {
68
+ return sNorm;
69
+ }
70
+
71
+ if (s.startsWith("/work/")) {
72
+ const rest = s.slice("/work/".length);
73
+ const parts = rest.split("/").filter(Boolean);
74
+ if (parts.length >= 2) {
75
+ const rel = parts.slice(1).join("/");
76
+ const relNative = _posixToNative(rel);
77
+ return path.join(root, relNative);
78
+ }
79
+ return p;
80
+ }
81
+
82
+ // If it's already a relative path, join it to the workspace root.
83
+ if (!s.startsWith("/") && !s.includes(":") && !s.includes("\\")) {
84
+ const relPosix = s.trim();
85
+ if (relPosix && relPosix !== "." && !relPosix.startsWith("../") && relPosix !== "..") {
86
+ const relNative = _posixToNative(relPosix);
87
+ const joined = path.join(root, relNative);
88
+ const relCheck = path.relative(root, joined);
89
+ if (relCheck && !relCheck.startsWith(`..${path.sep}`) && relCheck !== "..") {
90
+ return joined;
91
+ }
92
+ }
93
+ }
94
+
95
+ return p;
96
+ } catch {
97
+ return p;
98
+ }
99
+ }
100
+
101
+ function remapHitPaths(hit, workspaceRoot) {
102
+ if (!hit || typeof hit !== "object") {
103
+ return hit;
104
+ }
105
+ const rawPath = typeof hit.path === "string" ? hit.path : "";
106
+ let hostPath = typeof hit.host_path === "string" ? hit.host_path : "";
107
+ let containerPath = typeof hit.container_path === "string" ? hit.container_path : "";
108
+ if (!hostPath && rawPath) {
109
+ hostPath = rawPath;
110
+ }
111
+ if (!containerPath && rawPath) {
112
+ containerPath = rawPath;
113
+ }
114
+ const relPath = computeWorkspaceRelativePath(containerPath, hostPath);
115
+ const out = { ...hit };
116
+ if (relPath) {
117
+ out.rel_path = relPath;
118
+ }
119
+ // Remap related_paths nested under each hit (repo_search/hybrid_search emit this per result).
120
+ try {
121
+ if (Array.isArray(out.related_paths)) {
122
+ out.related_paths = out.related_paths.map((p) => remapRelatedPathToClient(p, workspaceRoot));
123
+ }
124
+ } catch {
125
+ // ignore
126
+ }
127
+ if (workspaceRoot && relPath) {
128
+ try {
129
+ const relNative = _posixToNative(relPath);
130
+ const candidate = path.join(workspaceRoot, relNative);
131
+ const diagnostics = envTruthy(process.env.CTXCE_BRIDGE_PATH_DIAGNOSTICS, false);
132
+ const strictClientPath = envTruthy(process.env.CTXCE_BRIDGE_CLIENT_PATH_STRICT, false);
133
+ if (strictClientPath) {
134
+ out.client_path = candidate;
135
+ if (diagnostics) {
136
+ out.client_path_joined = candidate;
137
+ out.client_path_source = "workspace_join";
138
+ }
139
+ } else {
140
+ // Prefer a host_path that is within the current bridge workspace.
141
+ // This keeps provenance (host_path) intact while providing a user-local
142
+ // absolute path even when the bridge workspace is a parent directory.
143
+ const hp = typeof hostPath === "string" ? hostPath : "";
144
+ const hpNorm = hp ? hp.replace(/\\/g, path.sep) : "";
145
+ if (
146
+ hpNorm &&
147
+ hpNorm.startsWith(workspaceRoot) &&
148
+ (!fs.existsSync(candidate) || fs.existsSync(hpNorm))
149
+ ) {
150
+ out.client_path = hpNorm;
151
+ if (diagnostics) {
152
+ out.client_path_joined = candidate;
153
+ out.client_path_source = "host_path";
154
+ }
155
+ } else {
156
+ out.client_path = candidate;
157
+ if (diagnostics) {
158
+ out.client_path_joined = candidate;
159
+ out.client_path_source = "workspace_join";
160
+ }
161
+ }
162
+ }
163
+ } catch {
164
+ // ignore
165
+ }
166
+ }
167
+ const overridePath = envTruthy(process.env.CTXCE_BRIDGE_OVERRIDE_PATH, true);
168
+ if (overridePath) {
169
+ if (typeof out.client_path === "string" && out.client_path) {
170
+ out.path = out.client_path;
171
+ } else if (relPath) {
172
+ out.path = relPath;
173
+ }
174
+ }
175
+ return out;
176
+ }
177
+
178
+ function remapStringPath(p, workspaceRoot) {
179
+ try {
180
+ const s = typeof p === "string" ? p : "";
181
+ if (!s) {
182
+ return p;
183
+ }
184
+ // If this is already a path within the current client workspace, rewrite to a
185
+ // workspace-relative string when override is enabled.
186
+ try {
187
+ const root = typeof workspaceRoot === "string" ? workspaceRoot : "";
188
+ if (root) {
189
+ const sNorm = s.replace(/\\/g, path.sep);
190
+ if (sNorm.startsWith(root + path.sep) || sNorm === root) {
191
+ const relNative = path.relative(root, sNorm);
192
+ const relPosix = String(relNative).split(path.sep).join("/");
193
+ if (relPosix && !relPosix.startsWith("../") && relPosix !== "..") {
194
+ const override = envTruthy(process.env.CTXCE_BRIDGE_OVERRIDE_PATH, true);
195
+ if (override) {
196
+ return relPosix;
197
+ }
198
+ }
199
+ }
200
+ }
201
+ } catch {
202
+ // ignore
203
+ }
204
+ if (s.startsWith("/work/")) {
205
+ const rest = s.slice("/work/".length);
206
+ const parts = rest.split("/").filter(Boolean);
207
+ if (parts.length >= 2) {
208
+ const rel = parts.slice(1).join("/");
209
+ const override = envTruthy(process.env.CTXCE_BRIDGE_OVERRIDE_PATH, true);
210
+ if (override) {
211
+ return rel;
212
+ }
213
+ return p;
214
+ }
215
+ }
216
+ return p;
217
+ } catch {
218
+ return p;
219
+ }
220
+ }
221
+
222
+ function maybeParseToolJson(result) {
223
+ try {
224
+ if (
225
+ result &&
226
+ typeof result === "object" &&
227
+ result.structuredContent &&
228
+ typeof result.structuredContent === "object"
229
+ ) {
230
+ return { mode: "structured", value: result.structuredContent };
231
+ }
232
+ } catch {
233
+ }
234
+ try {
235
+ const content = result && result.content;
236
+ if (!Array.isArray(content)) {
237
+ return null;
238
+ }
239
+ const first = content.find(
240
+ (c) => c && c.type === "text" && typeof c.text === "string",
241
+ );
242
+ if (!first) {
243
+ return null;
244
+ }
245
+ const txt = String(first.text || "").trim();
246
+ if (!txt || !(txt.startsWith("{") || txt.startsWith("["))) {
247
+ return null;
248
+ }
249
+ return { mode: "text", value: JSON.parse(txt) };
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+
255
+ function applyPathMappingToPayload(payload, workspaceRoot) {
256
+ if (!payload || typeof payload !== "object") {
257
+ return payload;
258
+ }
259
+ const out = Array.isArray(payload) ? payload.slice() : { ...payload };
260
+
261
+ const mapHitsArray = (arr) => {
262
+ if (!Array.isArray(arr)) {
263
+ return arr;
264
+ }
265
+ return arr.map((h) => remapHitPaths(h, workspaceRoot));
266
+ };
267
+
268
+ // Common result shapes across tools
269
+ if (Array.isArray(out.results)) {
270
+ out.results = mapHitsArray(out.results);
271
+ }
272
+ if (Array.isArray(out.citations)) {
273
+ out.citations = mapHitsArray(out.citations);
274
+ }
275
+ if (Array.isArray(out.related_paths)) {
276
+ out.related_paths = out.related_paths.map((p) => remapRelatedPathToClient(p, workspaceRoot));
277
+ }
278
+
279
+ // Some tools nest under {result:{...}}
280
+ if (out.result && typeof out.result === "object") {
281
+ out.result = applyPathMappingToPayload(out.result, workspaceRoot);
282
+ }
283
+
284
+ return out;
285
+ }
286
+
287
+ export function maybeRemapToolResult(name, result, workspaceRoot) {
288
+ try {
289
+ if (!name || !result || !workspaceRoot) {
290
+ return result;
291
+ }
292
+ const enabled = envTruthy(process.env.CTXCE_BRIDGE_MAP_PATHS, true);
293
+ if (!enabled) {
294
+ return result;
295
+ }
296
+ const lower = String(name).toLowerCase();
297
+ const shouldMap = (
298
+ lower === "repo_search" ||
299
+ lower === "context_search" ||
300
+ lower === "context_answer" ||
301
+ lower.endsWith("search_tests_for") ||
302
+ lower.endsWith("search_config_for") ||
303
+ lower.endsWith("search_callers_for") ||
304
+ lower.endsWith("search_importers_for")
305
+ );
306
+ if (!shouldMap) {
307
+ return result;
308
+ }
309
+
310
+ const parsed = maybeParseToolJson(result);
311
+ if (!parsed) {
312
+ return result;
313
+ }
314
+
315
+ const mapped = applyPathMappingToPayload(parsed.value, workspaceRoot);
316
+ let outResult = result;
317
+ if (parsed.mode === "structured") {
318
+ outResult = { ...result, structuredContent: mapped };
319
+ }
320
+
321
+ // Replace text payload for clients that only read `content[].text`
322
+ try {
323
+ const content = Array.isArray(outResult.content) ? outResult.content.slice() : [];
324
+ const idx = content.findIndex(
325
+ (c) => c && c.type === "text" && typeof c.text === "string",
326
+ );
327
+ if (idx >= 0) {
328
+ content[idx] = { ...content[idx], text: JSON.stringify(mapped) };
329
+ outResult = { ...outResult, content };
330
+ }
331
+ } catch {
332
+ // ignore
333
+ }
334
+ return outResult;
335
+ } catch {
336
+ return result;
337
+ }
338
+ }