@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 +1 -1
- package/src/mcpServer.js +102 -6
- package/src/oauthHandler.js +46 -3
- package/AGENTS.md +0 -18
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
|
912
|
-
|
|
913
|
-
//
|
|
914
|
-
//
|
|
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;
|
package/src/oauthHandler.js
CHANGED
|
@@ -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:
|
|
589
|
-
//
|
|
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`.
|