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