@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
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
@@ -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 authBackendUrl = process.env.CTXCE_AUTH_BACKEND_URL || "";
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 resolveSessionId() {
292
- const explicit = process.env.CTXCE_SESSION_ID || "";
293
- if (explicit) {
294
- return explicit;
397
+ function sessionFromEntry(entry) {
398
+ if (!entry || typeof entry.sessionId !== "string" || !entry.sessionId) {
399
+ return "";
295
400
  }
296
- let backendToUse = authBackendUrl;
297
- let entry = null;
298
- if (backendToUse) {
299
- try {
300
- entry = loadAuthEntry(backendToUse);
301
- } catch {
302
- entry = null;
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
- if (!entry) {
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 any = loadAnyAuthEntry();
308
- if (any && any.entry) {
309
- backendToUse = any.backendUrl;
310
- entry = any.entry;
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
- entry = null;
428
+ // ignore lookup failures
314
429
  }
315
430
  }
316
- if (entry) {
317
- let expired = false;
318
- const rawExpires = entry.expiresAt;
319
- if (typeof rawExpires === "number" && Number.isFinite(rawExpires) && rawExpires > 0) {
320
- const nowSecs = Math.floor(Date.now() / 1000);
321
- if (rawExpires < nowSecs) {
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
+
@@ -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 (relPath) {
264
- out.rel_path = relPath;
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
- if (workspaceRoot && relPath) {
280
+ const finalRelPath = out.rel_path || "";
281
+ if (workspaceRoot && finalRelPath) {
275
282
  try {
276
- const relNative = _posixToNative(relPath);
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 (relPath) {
319
- out.path = relPath;
325
+ } else if (finalRelPath) {
326
+ out.path = finalRelPath;
320
327
  }
321
328
  }
322
329
  return out;