@context-engine-bridge/context-engine-mcp-bridge 0.0.78 → 0.0.79

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.78",
3
+ "version": "0.0.79",
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
@@ -154,11 +154,19 @@ async function doLogin(args) {
154
154
  const sessionId = data.session_id || data.sessionId || null;
155
155
  const userId = data.user_id || data.userId || null;
156
156
  const expiresAt = data.expires_at || data.expiresAt || null;
157
+ const orgId = data.org_id || data.orgId || null;
158
+ const orgSlug = data.org_slug || data.orgSlug || null;
157
159
  if (!sessionId) {
158
160
  console.error("[ctxce] Auth login response missing session id.");
159
161
  process.exit(1);
160
162
  }
161
- saveAuthEntry(url, { sessionId, userId, expiresAt });
163
+ saveAuthEntry(url, {
164
+ sessionId,
165
+ userId,
166
+ expiresAt,
167
+ org_id: orgId,
168
+ org_slug: orgSlug,
169
+ });
162
170
  console.error("[ctxce] Auth login successful for", url);
163
171
  }
164
172
 
package/src/authConfig.js CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
 
5
5
  const CONFIG_DIR_NAME = ".ctxce";
6
6
  const CONFIG_BASENAME = "auth.json";
7
+ const RESOLVED_COLLECTIONS_KEY = "resolved_collections_v1";
7
8
 
8
9
  function getConfigPath() {
9
10
  const home = os.homedir() || process.cwd();
@@ -36,6 +37,38 @@ function writeConfig(data) {
36
37
  }
37
38
  }
38
39
 
40
+ function normalizeScopeValue(value) {
41
+ return typeof value === "string" ? value.trim() : "";
42
+ }
43
+
44
+ function buildResolvedCollectionScopeKey({ orgId, orgSlug, logicalRepoId } = {}) {
45
+ const repo = normalizeScopeValue(logicalRepoId);
46
+ if (!repo) {
47
+ return "";
48
+ }
49
+ const normalizedOrgId = normalizeScopeValue(orgId);
50
+ const normalizedOrgSlug = normalizeScopeValue(orgSlug);
51
+ const org = normalizedOrgId || (normalizedOrgSlug ? `slug:${normalizedOrgSlug}` : "org:none");
52
+ return `${org}::${repo}`;
53
+ }
54
+
55
+ function getResolvedCollections(entry) {
56
+ const map = entry && typeof entry === "object" ? entry[RESOLVED_COLLECTIONS_KEY] : null;
57
+ return map && typeof map === "object" ? map : {};
58
+ }
59
+
60
+ function preserveInternalEntryState(existingEntry, nextEntry) {
61
+ const preserved = {};
62
+ const existingCollections = getResolvedCollections(existingEntry);
63
+ if (Object.keys(existingCollections).length > 0) {
64
+ preserved[RESOLVED_COLLECTIONS_KEY] = existingCollections;
65
+ }
66
+ return {
67
+ ...preserved,
68
+ ...nextEntry,
69
+ };
70
+ }
71
+
39
72
  export function loadAuthEntry(backendUrl) {
40
73
  if (!backendUrl) {
41
74
  return null;
@@ -55,7 +88,8 @@ export function saveAuthEntry(backendUrl, entry) {
55
88
  }
56
89
  const all = readConfig();
57
90
  const key = String(backendUrl);
58
- all[key] = entry;
91
+ const existingEntry = all[key];
92
+ all[key] = preserveInternalEntryState(existingEntry, entry);
59
93
  writeConfig(all);
60
94
  }
61
95
 
@@ -82,3 +116,39 @@ export function loadAnyAuthEntry() {
82
116
  }
83
117
  return null;
84
118
  }
119
+
120
+ export function loadResolvedCollection(backendUrl, scope) {
121
+ if (!backendUrl) {
122
+ return null;
123
+ }
124
+ const scopeKey = buildResolvedCollectionScopeKey(scope);
125
+ if (!scopeKey) {
126
+ return null;
127
+ }
128
+ const entry = loadAuthEntry(backendUrl);
129
+ const resolved = getResolvedCollections(entry)[scopeKey];
130
+ return typeof resolved === "string" && resolved.trim() ? resolved.trim() : null;
131
+ }
132
+
133
+ export function saveResolvedCollection(backendUrl, scope, collection) {
134
+ if (!backendUrl) {
135
+ return;
136
+ }
137
+ const scopeKey = buildResolvedCollectionScopeKey(scope);
138
+ const nextCollection = typeof collection === "string" ? collection.trim() : "";
139
+ if (!scopeKey || !nextCollection) {
140
+ return;
141
+ }
142
+ const all = readConfig();
143
+ const key = String(backendUrl);
144
+ const existingEntry = all[key] && typeof all[key] === "object" ? all[key] : {};
145
+ const resolvedCollections = {
146
+ ...getResolvedCollections(existingEntry),
147
+ [scopeKey]: nextCollection,
148
+ };
149
+ all[key] = {
150
+ ...existingEntry,
151
+ [RESOLVED_COLLECTIONS_KEY]: resolvedCollections,
152
+ };
153
+ writeConfig(all);
154
+ }
package/src/mcpServer.js CHANGED
@@ -11,9 +11,17 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
11
11
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
12
12
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
13
13
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
14
- import { loadAnyAuthEntry, loadAuthEntry, readConfig, saveAuthEntry } from "./authConfig.js";
14
+ import {
15
+ loadAnyAuthEntry,
16
+ loadAuthEntry,
17
+ loadResolvedCollection,
18
+ readConfig,
19
+ saveAuthEntry,
20
+ saveResolvedCollection,
21
+ } from "./authConfig.js";
15
22
  import { maybeRemapToolArgs, maybeRemapToolResult } from "./resultPathMapping.js";
16
23
  import { startSyncDaemon } from "./syncDaemon.js";
24
+ import { computeLogicalRepoIdentity } from "./uploader.js";
17
25
  import * as oauthHandler from "./oauthHandler.js";
18
26
 
19
27
  const LSP_CONN_CACHE_TTL = 5000;
@@ -593,6 +601,7 @@ async function fetchBridgeCollectionState({
593
601
  collection,
594
602
  sessionId,
595
603
  repoName,
604
+ logicalRepoId,
596
605
  bridgeStateToken,
597
606
  backendHint,
598
607
  uploadServiceUrl,
@@ -612,6 +621,9 @@ async function fetchBridgeCollectionState({
612
621
  if (repoName && repoName.trim()) {
613
622
  url.searchParams.set("repo_name", repoName.trim());
614
623
  }
624
+ if (logicalRepoId && logicalRepoId.trim()) {
625
+ url.searchParams.set("logical_repo_id", logicalRepoId.trim());
626
+ }
615
627
 
616
628
  const headers = {
617
629
  Accept: "application/json",
@@ -646,6 +658,131 @@ async function fetchBridgeCollectionState({
646
658
  }
647
659
  }
648
660
 
661
+ export async function buildDefaultsPayload({
662
+ workspace,
663
+ sessionId,
664
+ explicitCollection,
665
+ defaultCollection,
666
+ defaultMode,
667
+ defaultUnder,
668
+ config,
669
+ backendHint,
670
+ uploadServiceUrl,
671
+ bridgeStateToken = BRIDGE_STATE_TOKEN,
672
+ authEntry = undefined,
673
+ fetchState = fetchBridgeCollectionState,
674
+ getLogicalRepoIdentity = computeLogicalRepoIdentity,
675
+ loadResolvedCollectionHint = loadResolvedCollection,
676
+ saveResolvedCollectionHint = saveResolvedCollection,
677
+ log = debugLog,
678
+ } = {}) {
679
+ const defaultsPayload = { session: sessionId };
680
+ const pinnedCollection =
681
+ typeof explicitCollection === "string" ? explicitCollection.trim() : "";
682
+ const configuredCollection =
683
+ !pinnedCollection && typeof defaultCollection === "string"
684
+ ? defaultCollection.trim()
685
+ : "";
686
+
687
+ if (pinnedCollection) {
688
+ defaultsPayload.collection = pinnedCollection;
689
+ } else if (configuredCollection) {
690
+ defaultsPayload.collection = configuredCollection;
691
+ }
692
+
693
+ const resolvedAuthEntry = authEntry === undefined
694
+ ? (backendHint ? loadAuthEntry(backendHint) : null)
695
+ : authEntry;
696
+
697
+ if (resolvedAuthEntry && (resolvedAuthEntry.org_id || resolvedAuthEntry.org_slug)) {
698
+ if (resolvedAuthEntry.org_id) {
699
+ defaultsPayload.org_id = resolvedAuthEntry.org_id;
700
+ }
701
+ if (resolvedAuthEntry.org_slug) {
702
+ defaultsPayload.org_slug = resolvedAuthEntry.org_slug;
703
+ }
704
+ }
705
+
706
+ const repoName = detectRepoName(workspace, config);
707
+ let logicalRepoId = "";
708
+ try {
709
+ logicalRepoId = getLogicalRepoIdentity(workspace)?.id || "";
710
+ } catch {
711
+ logicalRepoId = "";
712
+ }
713
+
714
+ const exactWorkspaceCollection = !pinnedCollection
715
+ ? _readExactWorkspaceCachedCollection(workspace)
716
+ : null;
717
+
718
+ if (!defaultsPayload.collection && exactWorkspaceCollection) {
719
+ defaultsPayload.collection = exactWorkspaceCollection;
720
+ log(`[ctxce] Using exact workspace cached collection: ${exactWorkspaceCollection}`);
721
+ }
722
+
723
+ if (!defaultsPayload.collection && backendHint && logicalRepoId) {
724
+ const cachedCollection = loadResolvedCollectionHint(backendHint, {
725
+ orgId: resolvedAuthEntry?.org_id,
726
+ orgSlug: resolvedAuthEntry?.org_slug,
727
+ logicalRepoId,
728
+ });
729
+ if (cachedCollection) {
730
+ defaultsPayload.collection = cachedCollection;
731
+ log(`[ctxce] Using cached resolved collection from auth store: ${cachedCollection}`);
732
+ }
733
+ }
734
+
735
+ if (!pinnedCollection) {
736
+ try {
737
+ const state = await fetchState({
738
+ workspace,
739
+ collection: configuredCollection,
740
+ sessionId,
741
+ repoName,
742
+ logicalRepoId,
743
+ bridgeStateToken,
744
+ backendHint,
745
+ uploadServiceUrl,
746
+ });
747
+ if (state) {
748
+ const servingCollection = typeof state.serving_collection === "string"
749
+ ? state.serving_collection.trim()
750
+ : "";
751
+ const activeCollection = typeof state.active_collection === "string"
752
+ ? state.active_collection.trim()
753
+ : "";
754
+ if (servingCollection) {
755
+ defaultsPayload.collection = servingCollection;
756
+ if (!configuredCollection || configuredCollection !== servingCollection) {
757
+ log(`[ctxce] Using serving collection from /bridge/state: ${servingCollection}`);
758
+ }
759
+ if (backendHint && logicalRepoId) {
760
+ saveResolvedCollectionHint(backendHint, {
761
+ orgId: resolvedAuthEntry?.org_id,
762
+ orgSlug: resolvedAuthEntry?.org_slug,
763
+ logicalRepoId,
764
+ }, servingCollection);
765
+ }
766
+ } else if (!defaultsPayload.collection && activeCollection) {
767
+ defaultsPayload.collection = activeCollection;
768
+ log(`[ctxce] Using active collection from /bridge/state fallback: ${activeCollection}`);
769
+ }
770
+ }
771
+ } catch (err) {
772
+ log("[ctxce] bridge/state lookup failed: " + String(err));
773
+ }
774
+ }
775
+
776
+ if (defaultMode) {
777
+ defaultsPayload.mode = defaultMode;
778
+ }
779
+ if (defaultUnder) {
780
+ defaultsPayload.under = defaultUnder;
781
+ }
782
+
783
+ return defaultsPayload;
784
+ }
785
+
649
786
  function _validateWorkspacePath(raw) {
650
787
  if (typeof raw !== "string" || raw.length === 0) return null;
651
788
  const resolved = path.resolve(raw);
@@ -659,6 +796,42 @@ function _validateWorkspacePath(raw) {
659
796
  return resolved;
660
797
  }
661
798
 
799
+ function _readExactWorkspaceCachedCollection(workspacePath) {
800
+ const resolvedWorkspace = _validateWorkspacePath(workspacePath);
801
+ if (!resolvedWorkspace) return null;
802
+
803
+ const wsDir = _computeWorkspaceDir(resolvedWorkspace);
804
+ const configPath = path.join(wsDir, "ctx_config.json");
805
+ const metaPath = path.join(wsDir, "meta.json");
806
+
807
+ if (!fs.existsSync(configPath)) {
808
+ return null;
809
+ }
810
+
811
+ if (fs.existsSync(metaPath)) {
812
+ try {
813
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
814
+ const metaWorkspace = _validateWorkspacePath(meta && meta.workspace_path);
815
+ if (!metaWorkspace || metaWorkspace !== resolvedWorkspace) {
816
+ return null;
817
+ }
818
+ } catch (_) {
819
+ return null;
820
+ }
821
+ }
822
+
823
+ try {
824
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
825
+ const collection =
826
+ parsed && typeof parsed.default_collection === "string"
827
+ ? parsed.default_collection.trim()
828
+ : "";
829
+ return collection || null;
830
+ } catch (_) {
831
+ return null;
832
+ }
833
+ }
834
+
662
835
  const MAX_WS_SCAN = 50;
663
836
 
664
837
  function _resolveWorkspace(providedWorkspace) {
@@ -877,55 +1050,17 @@ async function createBridgeServer(options) {
877
1050
 
878
1051
  // Best-effort: inform the indexer of default collection and session.
879
1052
  // If this fails we still proceed, falling back to per-call injection.
880
- const defaultsPayload = { session: sessionId };
881
- if (defaultCollection) {
882
- defaultsPayload.collection = defaultCollection;
883
- }
884
-
885
- // Include org context from auth entry if available (for org-scoped collection isolation)
886
- try {
887
- const authEntry = backendHint ? loadAuthEntry(backendHint) : null;
888
- if (authEntry && authEntry.org_id) {
889
- defaultsPayload.org_id = authEntry.org_id;
890
- defaultsPayload.org_slug = authEntry.org_slug;
891
- }
892
- } catch {
893
- // ignore auth entry lookup failures
894
- }
895
-
896
- const repoName = detectRepoName(workspace, config);
897
-
898
- try {
899
- const state = await fetchBridgeCollectionState({
900
- workspace,
901
- collection: defaultCollection,
902
- sessionId,
903
- repoName,
904
- bridgeStateToken: BRIDGE_STATE_TOKEN,
905
- backendHint,
906
- uploadServiceUrl,
907
- });
908
- if (state) {
909
- const serving = state.serving_collection || state.active_collection;
910
- if (serving) {
911
- defaultsPayload.collection = serving;
912
- if (!defaultCollection || defaultCollection !== serving) {
913
- debugLog(
914
- `[ctxce] Using serving collection from /bridge/state: ${serving}`,
915
- );
916
- }
917
- }
918
- }
919
- } catch (err) {
920
- debugLog("[ctxce] bridge/state lookup failed: " + String(err));
921
- }
922
-
923
- if (defaultMode) {
924
- defaultsPayload.mode = defaultMode;
925
- }
926
- if (defaultUnder) {
927
- defaultsPayload.under = defaultUnder;
928
- }
1053
+ const defaultsPayload = await buildDefaultsPayload({
1054
+ workspace,
1055
+ sessionId,
1056
+ explicitCollection,
1057
+ defaultCollection,
1058
+ defaultMode,
1059
+ defaultUnder,
1060
+ config,
1061
+ backendHint,
1062
+ uploadServiceUrl,
1063
+ });
929
1064
 
930
1065
  async function initializeRemoteClients(forceRecreate = false) {
931
1066
  if (!forceRecreate && indexerClient) {
@@ -247,6 +247,8 @@ export function getLoginPage(redirectUri, clientId, state, codeChallenge, codeCh
247
247
 
248
248
  const data = await resp.json();
249
249
  const sessionId = data.session_id || data.sessionId;
250
+ const orgId = data.org_id || data.orgId || null;
251
+ const orgSlug = data.org_slug || data.orgSlug || null;
250
252
  if (!sessionId) {
251
253
  throw new Error('No session in response');
252
254
  }
@@ -258,6 +260,8 @@ export function getLoginPage(redirectUri, clientId, state, codeChallenge, codeCh
258
260
  body: JSON.stringify({
259
261
  session_id: sessionId,
260
262
  backend_url: backendUrl,
263
+ org_id: orgId,
264
+ org_slug: orgSlug,
261
265
  redirect_uri: params.redirect_uri,
262
266
  state: params.state,
263
267
  code_challenge: params.code_challenge,
@@ -443,7 +447,17 @@ export function handleOAuthStoreSession(req, res) {
443
447
  res.setHeader("Content-Type", "application/json");
444
448
  try {
445
449
  const data = JSON.parse(body);
446
- const { session_id, backend_url, redirect_uri, state, code_challenge, code_challenge_method, client_id } = data;
450
+ const {
451
+ session_id,
452
+ backend_url,
453
+ org_id,
454
+ org_slug,
455
+ redirect_uri,
456
+ state,
457
+ code_challenge,
458
+ code_challenge_method,
459
+ client_id,
460
+ } = data;
447
461
 
448
462
  if (!session_id || !backend_url) {
449
463
  res.statusCode = 400;
@@ -503,6 +517,8 @@ export function handleOAuthStoreSession(req, res) {
503
517
  sessionId: session_id,
504
518
  userId: "oauth-user",
505
519
  expiresAt: null,
520
+ org_id: org_id || null,
521
+ org_slug: org_slug || null,
506
522
  });
507
523
 
508
524
  // Generate auth code
package/src/syncDaemon.js CHANGED
@@ -26,7 +26,7 @@ const noop = () => {};
26
26
 
27
27
  // ---------------------------------------------------------------------------
28
28
  // Process-level singleton registry
29
- // keyed by path.resolve(workspace) -> { intervalId, cleanup }
29
+ // keyed by path.resolve(workspace) -> { intervalId, cleanup, updateRuntimeState }
30
30
  // ---------------------------------------------------------------------------
31
31
 
32
32
  const _activeDaemons = new Map();
@@ -153,17 +153,24 @@ export function startSyncDaemon(options) {
153
153
  workspace,
154
154
  sessionId: initialSessionId,
155
155
  authEntry: initialAuthEntry,
156
- uploadEndpoint,
156
+ uploadEndpoint: initialUploadEndpoint,
157
157
  intervalMs = DEFAULT_WATCH_INTERVAL_MS,
158
158
  log = noop,
159
159
  } = options;
160
160
 
161
161
  const resolvedWorkspace = path.resolve(workspace);
162
162
 
163
- // Singleton guard: return existing daemon if already running.
163
+ // Singleton guard: return existing daemon if already running, but refresh
164
+ // any mutable runtime state supplied by the new caller.
164
165
  if (_activeDaemons.has(resolvedWorkspace)) {
166
+ const existingHandle = _activeDaemons.get(resolvedWorkspace);
167
+ existingHandle?.updateRuntimeState?.({
168
+ sessionId: initialSessionId,
169
+ authEntry: initialAuthEntry,
170
+ uploadEndpoint: initialUploadEndpoint,
171
+ });
165
172
  log(`[syncDaemon] Daemon already running for ${resolvedWorkspace}, reusing.`);
166
- return _activeDaemons.get(resolvedWorkspace);
173
+ return existingHandle;
167
174
  }
168
175
 
169
176
  log(`[syncDaemon] Starting file watcher for ${resolvedWorkspace} (interval: ${intervalMs / 1000}s)`);
@@ -174,9 +181,28 @@ export function startSyncDaemon(options) {
174
181
  let pendingHistoryOnly = true;
175
182
  let sessionId = initialSessionId;
176
183
  let authEntry = initialAuthEntry;
184
+ let uploadEndpoint = initialUploadEndpoint;
177
185
  let lastKnownHead = "";
178
186
 
179
- const authBackendUrl = deriveAuthBackendUrl(uploadEndpoint);
187
+ let authBackendUrl = deriveAuthBackendUrl(uploadEndpoint);
188
+
189
+ function updateRuntimeState(nextState = {}) {
190
+ if (typeof nextState.sessionId === "string" && nextState.sessionId) {
191
+ sessionId = nextState.sessionId;
192
+ }
193
+
194
+ if (nextState.authEntry && typeof nextState.authEntry === "object") {
195
+ authEntry = nextState.authEntry;
196
+ if (typeof nextState.authEntry.sessionId === "string" && nextState.authEntry.sessionId) {
197
+ sessionId = nextState.authEntry.sessionId;
198
+ }
199
+ }
200
+
201
+ if (typeof nextState.uploadEndpoint === "string" && nextState.uploadEndpoint) {
202
+ uploadEndpoint = nextState.uploadEndpoint;
203
+ authBackendUrl = deriveAuthBackendUrl(uploadEndpoint);
204
+ }
205
+ }
180
206
 
181
207
  // -------------------------------------------------------------------------
182
208
  // Session refresh
@@ -407,7 +433,7 @@ export function startSyncDaemon(options) {
407
433
  };
408
434
 
409
435
  // Register in singleton map BEFORE returning so concurrent callers see it.
410
- const handle = { intervalId, cleanup };
436
+ const handle = { intervalId, cleanup, updateRuntimeState };
411
437
  _activeDaemons.set(resolvedWorkspace, handle);
412
438
 
413
439
  return handle;