@context-engine-bridge/context-engine-mcp-bridge 0.0.17 → 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.17",
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
@@ -175,6 +175,36 @@ function isAuthRejectionError(error) {
175
175
  }
176
176
  }
177
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
+
178
208
  function getBridgeRetryAttempts() {
179
209
  try {
180
210
  const raw = process.env.CTXCE_TOOL_RETRY_ATTEMPTS;
@@ -520,8 +550,21 @@ async function createBridgeServer(options) {
520
550
  sessionId = resolveSessionId();
521
551
  }
522
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
523
555
  if (!sessionId) {
524
- 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
+ }
525
568
  }
526
569
 
527
570
  // Best-effort: inform the indexer of default collection and session.
@@ -531,6 +574,17 @@ async function createBridgeServer(options) {
531
574
  defaultsPayload.collection = defaultCollection;
532
575
  }
533
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
+
534
588
  const repoName = detectRepoName(workspace, config);
535
589
 
536
590
  try {
@@ -782,10 +836,13 @@ async function createBridgeServer(options) {
782
836
 
783
837
  // Backend auth rejection (mcp_auth.py ValidationError) - expire local auth
784
838
  if (isAuthRejectionError(err)) {
839
+ const serverUrl = backendHint || uploadServiceUrl || "unknown server";
785
840
  debugLog(
786
- "[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: ` +
787
842
  String(err),
788
843
  );
844
+ // Emit special notification for VS Code extension to detect and show toast
845
+ emitAuthErrorNotification(serverUrl, err);
789
846
  if (backendHint) {
790
847
  try {
791
848
  const entry = loadAuthEntry(backendHint);
@@ -796,6 +853,13 @@ async function createBridgeServer(options) {
796
853
  // ignore failures
797
854
  }
798
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
+ }
799
863
  }
800
864
 
801
865
  if (!isTransientToolError(err) || attempt === maxAttempts - 1) {
@@ -908,10 +972,42 @@ export async function runHttpMcpServer(options) {
908
972
  // Check Bearer token for MCP endpoint (accept /mcp and /mcp/ for compatibility)
909
973
  if (parsedUrl.pathname === "/mcp" || parsedUrl.pathname === "/mcp/") {
910
974
  const authHeader = req.headers["authorization"] || "";
911
- const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;
912
-
913
- // TODO: Validate token and inject session
914
- // 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
+ }
915
1011
 
916
1012
  if (req.method !== "POST") {
917
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`.