@context-engine-bridge/context-engine-mcp-bridge 0.0.16 → 0.0.18

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.16",
3
+ "version": "0.0.18",
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
@@ -126,7 +126,8 @@ function selectClientForTool(name, indexerClient, memoryClient) {
126
126
  return indexerClient;
127
127
  }
128
128
  const lowered = name.toLowerCase();
129
- if (memoryClient && (lowered.startsWith("memory.") || lowered.startsWith("mcp_memory_"))) {
129
+ // Route to memory server for any memory-prefixed tool
130
+ if (memoryClient && lowered.startsWith("memory")) {
130
131
  return memoryClient;
131
132
  }
132
133
  return indexerClient;
@@ -174,6 +175,36 @@ function isAuthRejectionError(error) {
174
175
  }
175
176
  }
176
177
 
178
+ /**
179
+ * Format an auth rejection error with actionable information for users.
180
+ * Includes the backend URL and a hint to sign in via VS Code command palette.
181
+ */
182
+ function formatAuthRejectionError(originalError, backendUrl) {
183
+ const originalMsg =
184
+ (originalError && typeof originalError.message === "string" && originalError.message) ||
185
+ (typeof originalError === "string" ? originalError : String(originalError || "Unknown auth error"));
186
+
187
+ const serverInfo = backendUrl ? ` (server: ${backendUrl})` : "";
188
+ const hint = "Run 'Context Engine: Sign In' from the VS Code command palette to authenticate.";
189
+
190
+ return `Authentication failed${serverInfo}: ${originalMsg}. ${hint}`;
191
+ }
192
+
193
+ /**
194
+ * Emit a special log line that the VS Code extension can detect to show a notification toast.
195
+ * Format: [ctxce:auth-error] JSON payload
196
+ */
197
+ function emitAuthErrorNotification(backendUrl, originalError) {
198
+ const payload = {
199
+ type: "auth_rejection",
200
+ backend: backendUrl || "unknown",
201
+ message: String(originalError?.message || originalError || "Authentication failed"),
202
+ hint: "Run 'Context Engine: Sign In' from the VS Code command palette",
203
+ };
204
+ // This special prefix allows the VS Code extension to detect auth errors in stderr
205
+ debugLog(`[ctxce:auth-error] ${JSON.stringify(payload)}`);
206
+ }
207
+
177
208
  function getBridgeRetryAttempts() {
178
209
  try {
179
210
  const raw = process.env.CTXCE_TOOL_RETRY_ATTEMPTS;
@@ -519,8 +550,21 @@ async function createBridgeServer(options) {
519
550
  sessionId = resolveSessionId();
520
551
  }
521
552
 
553
+ // Only fall back to deterministic session if auth is not configured
554
+ // If auth backend is configured but no session found, log warning instead of creating deterministic session
522
555
  if (!sessionId) {
523
- sessionId = `ctxce-${Buffer.from(workspace).toString("hex").slice(0, 24)}`;
556
+ if (authBackendUrl) {
557
+ // Auth is configured but no valid session - don't use deterministic fallback
558
+ debugLog(`[ctxce] WARNING: Auth backend configured (${authBackendUrl}) but no valid session found.`);
559
+ debugLog("[ctxce] To authenticate, run 'Context Engine: Sign In' from the VS Code command palette, or run `ctxce auth login` from the terminal.");
560
+ debugLog("[ctxce] Continuing with deterministic session for backward compatibility, but this may fail if backend requires auth.");
561
+ // Emit notification for VS Code extension
562
+ emitAuthErrorNotification(authBackendUrl, { message: "No valid session found - authentication required" });
563
+ sessionId = `ctxce-${Buffer.from(workspace).toString("hex").slice(0, 24)}`;
564
+ } else {
565
+ // No auth configured - use deterministic session for local-only operation
566
+ sessionId = `ctxce-${Buffer.from(workspace).toString("hex").slice(0, 24)}`;
567
+ }
524
568
  }
525
569
 
526
570
  // Best-effort: inform the indexer of default collection and session.
@@ -530,6 +574,17 @@ async function createBridgeServer(options) {
530
574
  defaultsPayload.collection = defaultCollection;
531
575
  }
532
576
 
577
+ // Include org context from auth entry if available (for org-scoped collection isolation)
578
+ try {
579
+ const authEntry = backendHint ? loadAuthEntry(backendHint) : null;
580
+ if (authEntry && authEntry.org_id) {
581
+ defaultsPayload.org_id = authEntry.org_id;
582
+ defaultsPayload.org_slug = authEntry.org_slug;
583
+ }
584
+ } catch {
585
+ // ignore auth entry lookup failures
586
+ }
587
+
533
588
  const repoName = detectRepoName(workspace, config);
534
589
 
535
590
  try {
@@ -781,10 +836,13 @@ async function createBridgeServer(options) {
781
836
 
782
837
  // Backend auth rejection (mcp_auth.py ValidationError) - expire local auth
783
838
  if (isAuthRejectionError(err)) {
839
+ const serverUrl = backendHint || uploadServiceUrl || "unknown server";
784
840
  debugLog(
785
- "[ctxce] tools/call: backend auth rejection; marking local session as expired: " +
841
+ `[ctxce] tools/call: backend auth rejection from ${serverUrl}; marking local session as expired: ` +
786
842
  String(err),
787
843
  );
844
+ // Emit special notification for VS Code extension to detect and show toast
845
+ emitAuthErrorNotification(serverUrl, err);
788
846
  if (backendHint) {
789
847
  try {
790
848
  const entry = loadAuthEntry(backendHint);
@@ -795,6 +853,13 @@ async function createBridgeServer(options) {
795
853
  // ignore failures
796
854
  }
797
855
  }
856
+ // Enhance error with actionable message before throwing
857
+ if (!isTransientToolError(err) || attempt === maxAttempts - 1) {
858
+ const enhancedMessage = formatAuthRejectionError(err, serverUrl);
859
+ const enhancedError = new Error(enhancedMessage);
860
+ enhancedError.cause = err;
861
+ throw enhancedError;
862
+ }
798
863
  }
799
864
 
800
865
  if (!isTransientToolError(err) || attempt === maxAttempts - 1) {
@@ -907,10 +972,42 @@ export async function runHttpMcpServer(options) {
907
972
  // Check Bearer token for MCP endpoint (accept /mcp and /mcp/ for compatibility)
908
973
  if (parsedUrl.pathname === "/mcp" || parsedUrl.pathname === "/mcp/") {
909
974
  const authHeader = req.headers["authorization"] || "";
910
- const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;
911
-
912
- // TODO: Validate token and inject session
913
- // For now, allow unauthenticated (backward compatible)
975
+ const bearerToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;
976
+
977
+ // ----------------------------------------------------------------
978
+ // AUTHENTICATION DESIGN: Permissive by default for backward compatibility
979
+ // ----------------------------------------------------------------
980
+ // The condition `bearerToken && hasTokenStore()` is INTENTIONALLY permissive:
981
+ //
982
+ // 1. PRE-EXISTING USERS (v3.0.0, local/dev mode):
983
+ // - No OAuth flow occurs → tokenStore remains empty
984
+ // - hasTokenStore() returns false → auth check skipped entirely
985
+ // - Requests proceed without authentication (local dev experience)
986
+ //
987
+ // 2. SAAS PLATFORM USERS (multi-tenant):
988
+ // - User completes OAuth flow → token stored in tokenStore
989
+ // - hasTokenStore() returns true → bearer token validation required
990
+ // - Invalid/missing tokens are rejected with 401
991
+ //
992
+ // WHY NOT `hasTokenStore() && !bearerToken` (require token when store exists)?
993
+ // - Mixed environments: Some clients may be local (no auth) while others
994
+ // are authenticated. Requiring auth globally after first login would
995
+ // break local dev workflows in hybrid setups.
996
+ // - The current design: "validate if provided, but don't require"
997
+ //
998
+ // SECURITY NOTE: If strict authentication is required for all clients once
999
+ // any user authenticates, add an environment flag like CTXCE_REQUIRE_AUTH=1
1000
+ // and check it here to enforce bearer tokens regardless of token store state.
1001
+ // ----------------------------------------------------------------
1002
+ if (bearerToken && oauthHandler.hasTokenStore()) {
1003
+ const sessionId = oauthHandler.lookupToken(bearerToken);
1004
+ if (!sessionId) {
1005
+ // Token provided but invalid - reject
1006
+ res.writeHead(401, { "Content-Type": "application/json" });
1007
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Invalid or expired bearer token" }, id: null }));
1008
+ return;
1009
+ }
1010
+ }
914
1011
 
915
1012
  if (req.method !== "POST") {
916
1013
  res.statusCode = 405;
@@ -534,7 +534,7 @@ export function handleOAuthStoreSession(req, res) {
534
534
  export function handleOAuthToken(req, res) {
535
535
  let body = "";
536
536
  req.on("data", (chunk) => { body += chunk; });
537
- req.on("end", () => {
537
+ req.on("end", async () => {
538
538
  try {
539
539
  const data = new URLSearchParams(body);
540
540
  const code = data.get("code");
@@ -585,8 +585,24 @@ export function handleOAuthToken(req, res) {
585
585
  return;
586
586
  }
587
587
 
588
- // TODO: Validate PKCE code_verifier against code_challenge
589
- // For now, skip validation (local bridge, trusted)
588
+ // TODO: PKCE validation - disabled for now, no clients implement it yet
589
+ // if (pendingData.codeChallenge && pendingData.codeChallengeMethod === "S256") {
590
+ // const codeVerifier = data.get("code_verifier");
591
+ // if (!codeVerifier) {
592
+ // pendingCodes.delete(code);
593
+ // res.statusCode = 400;
594
+ // res.end(JSON.stringify({ error: "invalid_grant", error_description: "code_verifier required for PKCE" }));
595
+ // return;
596
+ // }
597
+ // const crypto = await import("node:crypto");
598
+ // const expectedChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
599
+ // if (expectedChallenge !== pendingData.codeChallenge) {
600
+ // pendingCodes.delete(code);
601
+ // res.statusCode = 400;
602
+ // res.end(JSON.stringify({ error: "invalid_grant", error_description: "code_verifier validation failed" }));
603
+ // return;
604
+ // }
605
+ // }
590
606
 
591
607
  // Clean up expired tokens periodically to prevent unbounded growth
592
608
  cleanupExpiredTokens();
@@ -651,4 +667,31 @@ export function isOAuthEndpoint(pathname) {
651
667
  );
652
668
  }
653
669
 
670
+ /**
671
+ * Check if the token store has any entries (indicates auth is active)
672
+ * @returns {boolean}
673
+ */
674
+ export function hasTokenStore() {
675
+ return tokenStore.size > 0;
676
+ }
677
+
678
+ /**
679
+ * Look up a bearer token and return the associated session ID
680
+ * @param {string} token - Bearer token to validate
681
+ * @returns {string|null} - Session ID if valid, null otherwise
682
+ */
683
+ export function lookupToken(token) {
684
+ const entry = tokenStore.get(token);
685
+ if (!entry) return null;
686
+
687
+ // Check expiration
688
+ const tokenAge = Date.now() - entry.createdAt;
689
+ if (tokenAge > TOKEN_EXPIRY_MS) {
690
+ tokenStore.delete(token);
691
+ return null;
692
+ }
693
+
694
+ return entry.sessionId || null;
695
+ }
696
+
654
697
  startCleanupInterval();
package/AGENTS.md DELETED
@@ -1,18 +0,0 @@
1
- <!-- Parent: ../AGENTS.md -->
2
- # MCP Bridge CLI (ctxce)
3
-
4
- Node package that exposes indexer+memory as a single MCP server (stdio + HTTP). Optional OAuth/PKCE.
5
-
6
- ## WHERE TO LOOK
7
-
8
- | Task | Location | Notes |
9
- |------|----------|-------|
10
- | CLI entry | `ctx-mcp-bridge/bin/ctxce.js` | wrapper -> src/cli |
11
- | CLI logic | `ctx-mcp-bridge/src/cli.js` | command parsing |
12
- | MCP proxy | `ctx-mcp-bridge/src/mcpServer.js` | forwards tools |
13
- | OAuth/PKCE | `ctx-mcp-bridge/src/oauthHandler.js` | auth flow |
14
- | Path mapping | `ctx-mcp-bridge/src/resultPathMapping.js` | local <-> remote |
15
-
16
- ## KNOWN TODO
17
-
18
- - PKCE verifier validation is still TODO in `ctx-mcp-bridge/src/oauthHandler.js`.