@context-engine-bridge/context-engine-mcp-bridge 0.0.11 → 0.0.13
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 +197 -33
- package/src/resultPathMapping.js +14 -7
package/package.json
CHANGED
package/src/mcpServer.js
CHANGED
|
@@ -251,6 +251,111 @@ function isTransientToolError(error) {
|
|
|
251
251
|
// Acts as a low-level proxy for tools, forwarding tools/list and tools/call
|
|
252
252
|
// to the remote qdrant-indexer MCP server while adding a local `ping` tool.
|
|
253
253
|
|
|
254
|
+
const ADMIN_SESSION_COOKIE_NAME = "ctxce_session";
|
|
255
|
+
const SLUGGED_REPO_RE = /.+-[0-9a-f]{16}(?:_old)?$/i;
|
|
256
|
+
const BRIDGE_STATE_TOKEN = (process.env.CTXCE_BRIDGE_STATE_TOKEN || "").trim();
|
|
257
|
+
|
|
258
|
+
function normalizeBackendUrl(candidate) {
|
|
259
|
+
const trimmed = (candidate || "").trim();
|
|
260
|
+
if (!trimmed) {
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const parsed = new URL(trimmed);
|
|
265
|
+
if (parsed.protocol && parsed.host) {
|
|
266
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
// ignore parse failures
|
|
270
|
+
}
|
|
271
|
+
return trimmed.replace(/\/+$/, "");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function resolveAuthBackendContext() {
|
|
275
|
+
const envBackend = normalizeBackendUrl(process.env.CTXCE_AUTH_BACKEND_URL || "");
|
|
276
|
+
if (envBackend) {
|
|
277
|
+
return { backendUrl: envBackend, source: "CTXCE_AUTH_BACKEND_URL" };
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const any = loadAnyAuthEntry();
|
|
281
|
+
const stored = normalizeBackendUrl(any?.backendUrl || "");
|
|
282
|
+
if (stored) {
|
|
283
|
+
return { backendUrl: stored, source: "auth_entry" };
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
// ignore auth config read failures
|
|
287
|
+
}
|
|
288
|
+
return { backendUrl: "", source: "" };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const {
|
|
292
|
+
backendUrl: AUTH_BACKEND_URL,
|
|
293
|
+
source: AUTH_BACKEND_SOURCE,
|
|
294
|
+
} = resolveAuthBackendContext();
|
|
295
|
+
const UPLOAD_SERVICE_URL = AUTH_BACKEND_URL;
|
|
296
|
+
const UPLOAD_AUTH_BACKEND = AUTH_BACKEND_URL;
|
|
297
|
+
|
|
298
|
+
if (UPLOAD_SERVICE_URL) {
|
|
299
|
+
debugLog(`[ctxce] Upload/auth backend resolved from ${AUTH_BACKEND_SOURCE}: ${UPLOAD_SERVICE_URL}`);
|
|
300
|
+
} else {
|
|
301
|
+
debugLog("[ctxce] No auth backend detected; bridge/state overrides disabled.");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function fetchBridgeCollectionState({
|
|
305
|
+
workspace,
|
|
306
|
+
collection,
|
|
307
|
+
sessionId,
|
|
308
|
+
repoName,
|
|
309
|
+
bridgeStateToken,
|
|
310
|
+
}) {
|
|
311
|
+
try {
|
|
312
|
+
if (!UPLOAD_SERVICE_URL) {
|
|
313
|
+
debugLog("[ctxce] Skipping bridge/state fetch: no upload endpoint configured.");
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const url = new URL("/bridge/state", UPLOAD_SERVICE_URL);
|
|
317
|
+
if (collection && collection.trim()) {
|
|
318
|
+
url.searchParams.set("collection", collection.trim());
|
|
319
|
+
} else if (workspace && workspace.trim()) {
|
|
320
|
+
url.searchParams.set("workspace", workspace.trim());
|
|
321
|
+
}
|
|
322
|
+
if (repoName && repoName.trim()) {
|
|
323
|
+
url.searchParams.set("repo_name", repoName.trim());
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const headers = {
|
|
327
|
+
Accept: "application/json",
|
|
328
|
+
};
|
|
329
|
+
if (bridgeStateToken && bridgeStateToken.trim()) {
|
|
330
|
+
headers["X-Bridge-State-Token"] = bridgeStateToken.trim();
|
|
331
|
+
}
|
|
332
|
+
if (sessionId) {
|
|
333
|
+
headers.Cookie = `${ADMIN_SESSION_COOKIE_NAME}=${sessionId}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
debugLog(`[ctxce] Fetching bridge/state from ${url.toString()} (repo=${repoName || "<none>"}).`);
|
|
337
|
+
const resp = await fetch(url, {
|
|
338
|
+
method: "GET",
|
|
339
|
+
headers,
|
|
340
|
+
});
|
|
341
|
+
if (!resp.ok) {
|
|
342
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
343
|
+
debugLog(
|
|
344
|
+
`[ctxce] /bridge/state responded ${resp.status}; missing or invalid token/session, falling back to ctx_config defaults.`,
|
|
345
|
+
);
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
throw new Error(`bridge/state responded ${resp.status}`);
|
|
349
|
+
}
|
|
350
|
+
debugLog(`[ctxce] bridge/state responded ${resp.status}`);
|
|
351
|
+
const data = await resp.json();
|
|
352
|
+
return data && typeof data === "object" ? data : null;
|
|
353
|
+
} catch (err) {
|
|
354
|
+
debugLog("[ctxce] Failed to fetch /bridge/state: " + String(err));
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
254
359
|
async function createBridgeServer(options) {
|
|
255
360
|
const workspace = options.workspace || process.cwd();
|
|
256
361
|
const indexerUrl = options.indexerUrl;
|
|
@@ -285,53 +390,65 @@ async function createBridgeServer(options) {
|
|
|
285
390
|
// keep it deterministic per workspace to help the indexer reuse
|
|
286
391
|
// session-scoped defaults.
|
|
287
392
|
const explicitSession = process.env.CTXCE_SESSION_ID || "";
|
|
288
|
-
const
|
|
393
|
+
const authBackendEnv = (process.env.CTXCE_AUTH_BACKEND_URL || "").trim();
|
|
394
|
+
let backendHint = authBackendEnv || UPLOAD_AUTH_BACKEND || "";
|
|
289
395
|
let sessionId = explicitSession;
|
|
290
396
|
|
|
291
|
-
function
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return explicit;
|
|
397
|
+
function sessionFromEntry(entry) {
|
|
398
|
+
if (!entry || typeof entry.sessionId !== "string" || !entry.sessionId) {
|
|
399
|
+
return "";
|
|
295
400
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
401
|
+
const expiresAt = entry.expiresAt;
|
|
402
|
+
if (
|
|
403
|
+
typeof expiresAt === "number" &&
|
|
404
|
+
Number.isFinite(expiresAt) &&
|
|
405
|
+
expiresAt > 0 &&
|
|
406
|
+
expiresAt < Math.floor(Date.now() / 1000)
|
|
407
|
+
) {
|
|
408
|
+
debugLog("[ctxce] Stored auth session appears expired; please run `ctxce auth login` again.");
|
|
409
|
+
return "";
|
|
304
410
|
}
|
|
305
|
-
|
|
411
|
+
return entry.sessionId;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function findSavedSession(backends) {
|
|
415
|
+
for (const backend of backends) {
|
|
416
|
+
const trimmed = (backend || "").trim();
|
|
417
|
+
if (!trimmed) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
306
420
|
try {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
421
|
+
const entry = loadAuthEntry(trimmed);
|
|
422
|
+
const session = sessionFromEntry(entry);
|
|
423
|
+
if (session) {
|
|
424
|
+
backendHint = trimmed;
|
|
425
|
+
return session;
|
|
311
426
|
}
|
|
312
427
|
} catch {
|
|
313
|
-
|
|
428
|
+
// ignore lookup failures
|
|
314
429
|
}
|
|
315
430
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
expired = true;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
if (!expired && typeof entry.sessionId === "string" && entry.sessionId) {
|
|
326
|
-
return entry.sessionId;
|
|
327
|
-
}
|
|
328
|
-
if (expired) {
|
|
329
|
-
debugLog("[ctxce] Stored auth session appears expired; please run `ctxce auth login` again.");
|
|
431
|
+
try {
|
|
432
|
+
const any = loadAnyAuthEntry();
|
|
433
|
+
const session = any ? sessionFromEntry(any.entry) : "";
|
|
434
|
+
if (session && any?.backendUrl) {
|
|
435
|
+
backendHint = any.backendUrl;
|
|
436
|
+
return session;
|
|
330
437
|
}
|
|
438
|
+
} catch {
|
|
439
|
+
// ignore lookup failures
|
|
331
440
|
}
|
|
332
441
|
return "";
|
|
333
442
|
}
|
|
334
443
|
|
|
444
|
+
function resolveSessionId() {
|
|
445
|
+
const explicit = (process.env.CTXCE_SESSION_ID || "").trim();
|
|
446
|
+
if (explicit) {
|
|
447
|
+
return explicit;
|
|
448
|
+
}
|
|
449
|
+
return findSavedSession([backendHint, UPLOAD_AUTH_BACKEND, authBackendEnv]);
|
|
450
|
+
}
|
|
451
|
+
|
|
335
452
|
if (!sessionId) {
|
|
336
453
|
sessionId = resolveSessionId();
|
|
337
454
|
}
|
|
@@ -346,6 +463,32 @@ async function createBridgeServer(options) {
|
|
|
346
463
|
if (defaultCollection) {
|
|
347
464
|
defaultsPayload.collection = defaultCollection;
|
|
348
465
|
}
|
|
466
|
+
|
|
467
|
+
const repoName = detectRepoName(workspace, config);
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const state = await fetchBridgeCollectionState({
|
|
471
|
+
workspace,
|
|
472
|
+
collection: defaultCollection,
|
|
473
|
+
sessionId,
|
|
474
|
+
repoName,
|
|
475
|
+
bridgeStateToken: BRIDGE_STATE_TOKEN,
|
|
476
|
+
});
|
|
477
|
+
if (state) {
|
|
478
|
+
const serving = state.serving_collection || state.active_collection;
|
|
479
|
+
if (serving) {
|
|
480
|
+
defaultsPayload.collection = serving;
|
|
481
|
+
if (!defaultCollection || defaultCollection !== serving) {
|
|
482
|
+
debugLog(
|
|
483
|
+
`[ctxce] Using serving collection from /bridge/state: ${serving}`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
} catch (err) {
|
|
489
|
+
debugLog("[ctxce] bridge/state lookup failed: " + String(err));
|
|
490
|
+
}
|
|
491
|
+
|
|
349
492
|
if (defaultMode) {
|
|
350
493
|
defaultsPayload.mode = defaultMode;
|
|
351
494
|
}
|
|
@@ -805,3 +948,24 @@ function detectGitBranch(workspace) {
|
|
|
805
948
|
}
|
|
806
949
|
}
|
|
807
950
|
|
|
951
|
+
function detectRepoName(workspace, config) {
|
|
952
|
+
const envRepo =
|
|
953
|
+
(process.env.CURRENT_REPO && process.env.CURRENT_REPO.trim()) ||
|
|
954
|
+
(process.env.REPO_NAME && process.env.REPO_NAME.trim());
|
|
955
|
+
if (envRepo) {
|
|
956
|
+
return envRepo;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (config) {
|
|
960
|
+
const cfgRepo =
|
|
961
|
+
(typeof config.repo_name === "string" && config.repo_name.trim()) ||
|
|
962
|
+
(typeof config.default_repo === "string" && config.default_repo.trim());
|
|
963
|
+
if (cfgRepo) {
|
|
964
|
+
return cfgRepo;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const leaf = workspace ? path.basename(workspace) : "";
|
|
969
|
+
return leaf && SLUGGED_REPO_RE.test(leaf) ? leaf : null;
|
|
970
|
+
}
|
|
971
|
+
|
package/src/resultPathMapping.js
CHANGED
|
@@ -258,10 +258,16 @@ function remapHitPaths(hit, workspaceRoot) {
|
|
|
258
258
|
if (!containerPath && rawPath) {
|
|
259
259
|
containerPath = rawPath;
|
|
260
260
|
}
|
|
261
|
-
const relPath = computeWorkspaceRelativePath(containerPath, hostPath);
|
|
262
261
|
const out = { ...hit };
|
|
263
|
-
if
|
|
264
|
-
|
|
262
|
+
// Respect server's rel_path if already provided and non-empty; only compute if missing
|
|
263
|
+
const serverRelPath = typeof hit.rel_path === "string" ? hit.rel_path.trim() : "";
|
|
264
|
+
if (serverRelPath) {
|
|
265
|
+
out.rel_path = serverRelPath;
|
|
266
|
+
} else {
|
|
267
|
+
const relPath = computeWorkspaceRelativePath(containerPath, hostPath);
|
|
268
|
+
if (relPath) {
|
|
269
|
+
out.rel_path = relPath;
|
|
270
|
+
}
|
|
265
271
|
}
|
|
266
272
|
// Remap related_paths nested under each hit (repo_search/hybrid_search emit this per result).
|
|
267
273
|
try {
|
|
@@ -271,9 +277,10 @@ function remapHitPaths(hit, workspaceRoot) {
|
|
|
271
277
|
} catch {
|
|
272
278
|
// ignore
|
|
273
279
|
}
|
|
274
|
-
|
|
280
|
+
const finalRelPath = out.rel_path || "";
|
|
281
|
+
if (workspaceRoot && finalRelPath) {
|
|
275
282
|
try {
|
|
276
|
-
const relNative = _posixToNative(
|
|
283
|
+
const relNative = _posixToNative(finalRelPath);
|
|
277
284
|
const candidate = path.join(workspaceRoot, relNative);
|
|
278
285
|
const diagnostics = envTruthy(process.env.CTXCE_BRIDGE_PATH_DIAGNOSTICS, false);
|
|
279
286
|
const strictClientPath = envTruthy(process.env.CTXCE_BRIDGE_CLIENT_PATH_STRICT, false);
|
|
@@ -315,8 +322,8 @@ function remapHitPaths(hit, workspaceRoot) {
|
|
|
315
322
|
if (overridePath) {
|
|
316
323
|
if (typeof out.client_path === "string" && out.client_path) {
|
|
317
324
|
out.path = out.client_path;
|
|
318
|
-
} else if (
|
|
319
|
-
out.path =
|
|
325
|
+
} else if (finalRelPath) {
|
|
326
|
+
out.path = finalRelPath;
|
|
320
327
|
}
|
|
321
328
|
}
|
|
322
329
|
return out;
|