@context-engine-bridge/context-engine-mcp-bridge 0.0.17 → 0.0.19
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/.claude/settings.local.json +11 -0
- package/.sisyphus/ultrawork-state.json +7 -0
- package/package.json +10 -2
- package/src/mcpServer.js +219 -56
- package/src/oauthHandler.js +46 -3
- package/AGENTS.md +0 -18
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"active": true,
|
|
3
|
+
"started_at": "2026-01-25T14:25:53.146Z",
|
|
4
|
+
"original_prompt": "/oh-my-claude-sisyphus:ultrawork we need to figure out, research context engine a bit.... how to make our agent tools better via mcp... (the claude.example.md is\n clear) but maybe we need an agents.md, we have claude skills... can we make the descriptors better? research alot fo other\n tools and see",
|
|
5
|
+
"reinforcement_count": 5,
|
|
6
|
+
"last_checked_at": "2026-01-25T14:43:28.449Z"
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@context-engine-bridge/context-engine-mcp-bridge",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "Context Engine MCP bridge (http/stdio proxy combining indexer + memory servers)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ctxce": "bin/ctxce.js",
|
|
@@ -9,12 +9,20 @@
|
|
|
9
9
|
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node bin/ctxce.js",
|
|
12
|
-
"postinstall": "node -e \"try{require('fs').chmodSync('bin/ctxce.js',0o755)}catch(e){}\""
|
|
12
|
+
"postinstall": "node -e \"try{require('fs').chmodSync('bin/ctxce.js',0o755)}catch(e){}\"",
|
|
13
|
+
"test:e2e": "playwright test",
|
|
14
|
+
"test:e2e:auth": "playwright test --project=auth-enforcement",
|
|
15
|
+
"test:e2e:happy": "playwright test --project=happy-paths-chromium",
|
|
16
|
+
"test:e2e:edge": "playwright test --project=edge-cases",
|
|
17
|
+
"test:e2e:ui": "playwright test --ui"
|
|
13
18
|
},
|
|
14
19
|
"dependencies": {
|
|
15
20
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
16
21
|
"zod": "^3.25.0"
|
|
17
22
|
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@playwright/test": "^1.57.0"
|
|
25
|
+
},
|
|
18
26
|
"publishConfig": {
|
|
19
27
|
"access": "public"
|
|
20
28
|
},
|
package/src/mcpServer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { execSync } from "node:child_process";
|
|
5
6
|
import { createServer } from "node:http";
|
|
6
7
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -175,6 +176,36 @@ function isAuthRejectionError(error) {
|
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Format an auth rejection error with actionable information for users.
|
|
181
|
+
* Includes the backend URL and a hint to sign in via VS Code command palette.
|
|
182
|
+
*/
|
|
183
|
+
function formatAuthRejectionError(originalError, backendUrl) {
|
|
184
|
+
const originalMsg =
|
|
185
|
+
(originalError && typeof originalError.message === "string" && originalError.message) ||
|
|
186
|
+
(typeof originalError === "string" ? originalError : String(originalError || "Unknown auth error"));
|
|
187
|
+
|
|
188
|
+
const serverInfo = backendUrl ? ` (server: ${backendUrl})` : "";
|
|
189
|
+
const hint = "Run 'Context Engine: Sign In' from the VS Code command palette to authenticate.";
|
|
190
|
+
|
|
191
|
+
return `Authentication failed${serverInfo}: ${originalMsg}. ${hint}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Emit a special log line that the VS Code extension can detect to show a notification toast.
|
|
196
|
+
* Format: [ctxce:auth-error] JSON payload
|
|
197
|
+
*/
|
|
198
|
+
function emitAuthErrorNotification(backendUrl, originalError) {
|
|
199
|
+
const payload = {
|
|
200
|
+
type: "auth_rejection",
|
|
201
|
+
backend: backendUrl || "unknown",
|
|
202
|
+
message: String(originalError?.message || originalError || "Authentication failed"),
|
|
203
|
+
hint: "Run 'Context Engine: Sign In' from the VS Code command palette",
|
|
204
|
+
};
|
|
205
|
+
// This special prefix allows the VS Code extension to detect auth errors in stderr
|
|
206
|
+
debugLog(`[ctxce:auth-error] ${JSON.stringify(payload)}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
178
209
|
function getBridgeRetryAttempts() {
|
|
179
210
|
try {
|
|
180
211
|
const raw = process.env.CTXCE_TOOL_RETRY_ATTEMPTS;
|
|
@@ -520,8 +551,21 @@ async function createBridgeServer(options) {
|
|
|
520
551
|
sessionId = resolveSessionId();
|
|
521
552
|
}
|
|
522
553
|
|
|
554
|
+
// Only fall back to deterministic session if auth is not configured
|
|
555
|
+
// If auth backend is configured but no session found, log warning instead of creating deterministic session
|
|
523
556
|
if (!sessionId) {
|
|
524
|
-
|
|
557
|
+
if (authBackendUrl) {
|
|
558
|
+
// Auth is configured but no valid session - don't use deterministic fallback
|
|
559
|
+
debugLog(`[ctxce] WARNING: Auth backend configured (${authBackendUrl}) but no valid session found.`);
|
|
560
|
+
debugLog("[ctxce] To authenticate, run 'Context Engine: Sign In' from the VS Code command palette, or run `ctxce auth login` from the terminal.");
|
|
561
|
+
debugLog("[ctxce] Continuing with deterministic session for backward compatibility, but this may fail if backend requires auth.");
|
|
562
|
+
// Emit notification for VS Code extension
|
|
563
|
+
emitAuthErrorNotification(authBackendUrl, { message: "No valid session found - authentication required" });
|
|
564
|
+
sessionId = `ctxce-${Buffer.from(workspace).toString("hex").slice(0, 24)}`;
|
|
565
|
+
} else {
|
|
566
|
+
// No auth configured - use deterministic session for local-only operation
|
|
567
|
+
sessionId = `ctxce-${Buffer.from(workspace).toString("hex").slice(0, 24)}`;
|
|
568
|
+
}
|
|
525
569
|
}
|
|
526
570
|
|
|
527
571
|
// Best-effort: inform the indexer of default collection and session.
|
|
@@ -531,6 +575,17 @@ async function createBridgeServer(options) {
|
|
|
531
575
|
defaultsPayload.collection = defaultCollection;
|
|
532
576
|
}
|
|
533
577
|
|
|
578
|
+
// Include org context from auth entry if available (for org-scoped collection isolation)
|
|
579
|
+
try {
|
|
580
|
+
const authEntry = backendHint ? loadAuthEntry(backendHint) : null;
|
|
581
|
+
if (authEntry && authEntry.org_id) {
|
|
582
|
+
defaultsPayload.org_id = authEntry.org_id;
|
|
583
|
+
defaultsPayload.org_slug = authEntry.org_slug;
|
|
584
|
+
}
|
|
585
|
+
} catch {
|
|
586
|
+
// ignore auth entry lookup failures
|
|
587
|
+
}
|
|
588
|
+
|
|
534
589
|
const repoName = detectRepoName(workspace, config);
|
|
535
590
|
|
|
536
591
|
try {
|
|
@@ -653,7 +708,7 @@ async function createBridgeServer(options) {
|
|
|
653
708
|
|
|
654
709
|
await initializeRemoteClients(false);
|
|
655
710
|
|
|
656
|
-
const server = new Server(
|
|
711
|
+
const server = new Server(
|
|
657
712
|
{
|
|
658
713
|
name: "ctx-context-engine-bridge",
|
|
659
714
|
version: "0.0.1",
|
|
@@ -782,10 +837,13 @@ async function createBridgeServer(options) {
|
|
|
782
837
|
|
|
783
838
|
// Backend auth rejection (mcp_auth.py ValidationError) - expire local auth
|
|
784
839
|
if (isAuthRejectionError(err)) {
|
|
840
|
+
const serverUrl = backendHint || uploadServiceUrl || "unknown server";
|
|
785
841
|
debugLog(
|
|
786
|
-
|
|
842
|
+
`[ctxce] tools/call: backend auth rejection from ${serverUrl}; marking local session as expired: ` +
|
|
787
843
|
String(err),
|
|
788
844
|
);
|
|
845
|
+
// Emit special notification for VS Code extension to detect and show toast
|
|
846
|
+
emitAuthErrorNotification(serverUrl, err);
|
|
789
847
|
if (backendHint) {
|
|
790
848
|
try {
|
|
791
849
|
const entry = loadAuthEntry(backendHint);
|
|
@@ -796,6 +854,13 @@ async function createBridgeServer(options) {
|
|
|
796
854
|
// ignore failures
|
|
797
855
|
}
|
|
798
856
|
}
|
|
857
|
+
// Enhance error with actionable message before throwing
|
|
858
|
+
if (!isTransientToolError(err) || attempt === maxAttempts - 1) {
|
|
859
|
+
const enhancedMessage = formatAuthRejectionError(err, serverUrl);
|
|
860
|
+
const enhancedError = new Error(enhancedMessage);
|
|
861
|
+
enhancedError.cause = err;
|
|
862
|
+
throw enhancedError;
|
|
863
|
+
}
|
|
799
864
|
}
|
|
800
865
|
|
|
801
866
|
if (!isTransientToolError(err) || attempt === maxAttempts - 1) {
|
|
@@ -844,17 +909,73 @@ export async function runMcpServer(options) {
|
|
|
844
909
|
}
|
|
845
910
|
|
|
846
911
|
export async function runHttpMcpServer(options) {
|
|
847
|
-
|
|
912
|
+
// Multi-client HTTP mode: each MCP client (Claude, Codex, Kiro, Windsurf,
|
|
913
|
+
// Augment, etc.) gets its own session with a dedicated Server+Transport
|
|
914
|
+
// pair. Sessions are tracked by the Mcp-Session-Id header. This allows
|
|
915
|
+
// multiple IDEs to share the same bridge simultaneously.
|
|
916
|
+
//
|
|
917
|
+
// Flow:
|
|
918
|
+
// 1. Client sends POST with initialize request (no session ID)
|
|
919
|
+
// 2. Bridge creates a new Server+Transport, generates a session ID
|
|
920
|
+
// 3. Response includes Mcp-Session-Id header
|
|
921
|
+
// 4. Client includes Mcp-Session-Id on all subsequent requests
|
|
922
|
+
// 5. Bridge routes to the correct Server+Transport for that session
|
|
923
|
+
//
|
|
924
|
+
// Each session has its own upstream connections (indexer, memory) created
|
|
925
|
+
// by createBridgeServer. Sessions are cleaned up after 30 min of inactivity.
|
|
926
|
+
|
|
848
927
|
const port =
|
|
849
928
|
typeof options.port === "number"
|
|
850
929
|
? options.port
|
|
851
930
|
: Number.parseInt(process.env.CTXCE_HTTP_PORT || "30810", 10) || 30810;
|
|
852
931
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
932
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
933
|
+
// Session management
|
|
934
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
935
|
+
const sessions = new Map(); // sessionId → { server, transport, lastUsed }
|
|
936
|
+
const SESSION_TTL_MS = 30 * 60 * 1000; // 30 minutes inactive
|
|
937
|
+
const MAX_SESSIONS = 20;
|
|
938
|
+
|
|
939
|
+
// Periodic cleanup of stale sessions
|
|
940
|
+
const cleanupInterval = setInterval(() => {
|
|
941
|
+
const now = Date.now();
|
|
942
|
+
for (const [sid, session] of sessions) {
|
|
943
|
+
if (now - session.lastUsed > SESSION_TTL_MS) {
|
|
944
|
+
debugLog(`[ctxce] Cleaning up stale MCP session: ${sid}`);
|
|
945
|
+
session.transport.close().catch(() => {});
|
|
946
|
+
session.server.close().catch(() => {});
|
|
947
|
+
sessions.delete(sid);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}, 60_000);
|
|
951
|
+
cleanupInterval.unref();
|
|
952
|
+
|
|
953
|
+
function evictOldestSession() {
|
|
954
|
+
if (sessions.size < MAX_SESSIONS) return;
|
|
955
|
+
let oldest = null;
|
|
956
|
+
let oldestTime = Infinity;
|
|
957
|
+
for (const [sid, s] of sessions) {
|
|
958
|
+
if (s.lastUsed < oldestTime) {
|
|
959
|
+
oldest = sid;
|
|
960
|
+
oldestTime = s.lastUsed;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (oldest) {
|
|
964
|
+
const s = sessions.get(oldest);
|
|
965
|
+
sessions.delete(oldest);
|
|
966
|
+
s.transport.close().catch(() => {});
|
|
967
|
+
s.server.close().catch(() => {});
|
|
968
|
+
debugLog(`[ctxce] Evicted oldest MCP session: ${oldest}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
856
971
|
|
|
857
|
-
|
|
972
|
+
// Pre-warm: validate upstream connectivity (best-effort)
|
|
973
|
+
try {
|
|
974
|
+
await createBridgeServer(options);
|
|
975
|
+
debugLog("[ctxce] HTTP bridge validated upstream connectivity");
|
|
976
|
+
} catch (err) {
|
|
977
|
+
debugLog("[ctxce] HTTP bridge warmup failed (non-fatal): " + String(err));
|
|
978
|
+
}
|
|
858
979
|
|
|
859
980
|
// Build issuer URL for OAuth
|
|
860
981
|
// Note: Local-only bridge uses 127.0.0.1. For remote access, this would need to be configurable.
|
|
@@ -905,24 +1026,57 @@ export async function runHttpMcpServer(options) {
|
|
|
905
1026
|
// MCP Endpoint
|
|
906
1027
|
// ================================================================
|
|
907
1028
|
|
|
908
|
-
//
|
|
1029
|
+
// Accept /mcp and /mcp/ for compatibility
|
|
909
1030
|
if (parsedUrl.pathname === "/mcp" || parsedUrl.pathname === "/mcp/") {
|
|
1031
|
+
// ── Bearer token auth (permissive: validate if provided, don't require) ──
|
|
910
1032
|
const authHeader = req.headers["authorization"] || "";
|
|
911
|
-
const
|
|
1033
|
+
const bearerToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
1034
|
+
if (bearerToken && oauthHandler.hasTokenStore()) {
|
|
1035
|
+
const oauthSessionId = oauthHandler.lookupToken(bearerToken);
|
|
1036
|
+
if (!oauthSessionId) {
|
|
1037
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1038
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Invalid or expired bearer token" }, id: null }));
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const mcpSessionId = typeof req.headers["mcp-session-id"] === "string"
|
|
1044
|
+
? req.headers["mcp-session-id"]
|
|
1045
|
+
: undefined;
|
|
1046
|
+
|
|
1047
|
+
// ── GET: SSE streaming for existing session ──
|
|
1048
|
+
if (req.method === "GET") {
|
|
1049
|
+
if (mcpSessionId && sessions.has(mcpSessionId)) {
|
|
1050
|
+
const session = sessions.get(mcpSessionId);
|
|
1051
|
+
session.lastUsed = Date.now();
|
|
1052
|
+
session.transport.handleRequest(req, res).catch((err) => {
|
|
1053
|
+
debugLog("[ctxce] SSE stream error: " + String(err));
|
|
1054
|
+
});
|
|
1055
|
+
} else {
|
|
1056
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1057
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request: No valid session for SSE" }, id: null }));
|
|
1058
|
+
}
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
912
1061
|
|
|
913
|
-
//
|
|
914
|
-
|
|
1062
|
+
// ── DELETE: terminate session ──
|
|
1063
|
+
if (req.method === "DELETE") {
|
|
1064
|
+
if (mcpSessionId && sessions.has(mcpSessionId)) {
|
|
1065
|
+
const session = sessions.get(mcpSessionId);
|
|
1066
|
+
sessions.delete(mcpSessionId);
|
|
1067
|
+
session.transport.close().catch(() => {});
|
|
1068
|
+
session.server.close().catch(() => {});
|
|
1069
|
+
debugLog(`[ctxce] Session terminated by client: ${mcpSessionId}`);
|
|
1070
|
+
}
|
|
1071
|
+
res.writeHead(200).end();
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
915
1074
|
|
|
1075
|
+
// ── POST: MCP JSON-RPC requests ──
|
|
916
1076
|
if (req.method !== "POST") {
|
|
917
1077
|
res.statusCode = 405;
|
|
918
1078
|
res.setHeader("Content-Type", "application/json");
|
|
919
|
-
res.end(
|
|
920
|
-
JSON.stringify({
|
|
921
|
-
jsonrpc: "2.0",
|
|
922
|
-
error: { code: -32000, message: "Method not allowed" },
|
|
923
|
-
id: null,
|
|
924
|
-
}),
|
|
925
|
-
);
|
|
1079
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed" }, id: null }));
|
|
926
1080
|
return;
|
|
927
1081
|
}
|
|
928
1082
|
|
|
@@ -937,13 +1091,7 @@ export async function runHttpMcpServer(options) {
|
|
|
937
1091
|
req.destroy();
|
|
938
1092
|
res.statusCode = 413;
|
|
939
1093
|
res.setHeader("Content-Type", "application/json");
|
|
940
|
-
res.end(
|
|
941
|
-
JSON.stringify({
|
|
942
|
-
jsonrpc: "2.0",
|
|
943
|
-
error: { code: -32000, message: "Request body too large" },
|
|
944
|
-
id: null,
|
|
945
|
-
}),
|
|
946
|
-
);
|
|
1094
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Request body too large" }, id: null }));
|
|
947
1095
|
}
|
|
948
1096
|
});
|
|
949
1097
|
req.on("end", async () => {
|
|
@@ -955,30 +1103,50 @@ export async function runHttpMcpServer(options) {
|
|
|
955
1103
|
debugLog("[ctxce] Failed to parse HTTP MCP request body: " + String(err));
|
|
956
1104
|
res.statusCode = 400;
|
|
957
1105
|
res.setHeader("Content-Type", "application/json");
|
|
958
|
-
res.end(
|
|
959
|
-
JSON.stringify({
|
|
960
|
-
jsonrpc: "2.0",
|
|
961
|
-
error: { code: -32700, message: "Invalid JSON" },
|
|
962
|
-
id: null,
|
|
963
|
-
}),
|
|
964
|
-
);
|
|
1106
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32700, message: "Invalid JSON" }, id: null }));
|
|
965
1107
|
return;
|
|
966
1108
|
}
|
|
967
1109
|
|
|
968
1110
|
try {
|
|
1111
|
+
// ── Route to existing session ──
|
|
1112
|
+
if (mcpSessionId && sessions.has(mcpSessionId)) {
|
|
1113
|
+
const session = sessions.get(mcpSessionId);
|
|
1114
|
+
session.lastUsed = Date.now();
|
|
1115
|
+
await session.transport.handleRequest(req, res, parsed);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// ── Unknown session ID (expired or invalid) ──
|
|
1120
|
+
if (mcpSessionId) {
|
|
1121
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1122
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Session not found. Client must re-initialize." }, id: null }));
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// ── New session (no session ID = first request / initialize) ──
|
|
1127
|
+
evictOldestSession();
|
|
1128
|
+
|
|
1129
|
+
const server = await createBridgeServer(options);
|
|
1130
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1131
|
+
sessionIdGenerator: () => randomUUID(),
|
|
1132
|
+
onsessioninitialized: (newSessionId) => {
|
|
1133
|
+
debugLog(`[ctxce] New MCP session: ${newSessionId} (active: ${sessions.size + 1})`);
|
|
1134
|
+
sessions.set(newSessionId, { server, transport, lastUsed: Date.now() });
|
|
1135
|
+
},
|
|
1136
|
+
onsessionclosed: (closedSessionId) => {
|
|
1137
|
+
debugLog(`[ctxce] MCP session closed by transport: ${closedSessionId}`);
|
|
1138
|
+
sessions.delete(closedSessionId);
|
|
1139
|
+
server.close().catch(() => {});
|
|
1140
|
+
},
|
|
1141
|
+
});
|
|
1142
|
+
await server.connect(transport);
|
|
969
1143
|
await transport.handleRequest(req, res, parsed);
|
|
970
1144
|
} catch (err) {
|
|
971
1145
|
debugLog("[ctxce] Error handling HTTP MCP request: " + String(err));
|
|
972
1146
|
if (!res.headersSent) {
|
|
973
1147
|
res.statusCode = 500;
|
|
974
1148
|
res.setHeader("Content-Type", "application/json");
|
|
975
|
-
res.end(
|
|
976
|
-
JSON.stringify({
|
|
977
|
-
jsonrpc: "2.0",
|
|
978
|
-
error: { code: -32603, message: "Internal server error" },
|
|
979
|
-
id: null,
|
|
980
|
-
}),
|
|
981
|
-
);
|
|
1149
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }));
|
|
982
1150
|
}
|
|
983
1151
|
}
|
|
984
1152
|
});
|
|
@@ -988,39 +1156,34 @@ export async function runHttpMcpServer(options) {
|
|
|
988
1156
|
// 404 for everything else
|
|
989
1157
|
res.statusCode = 404;
|
|
990
1158
|
res.setHeader("Content-Type", "application/json");
|
|
991
|
-
res.end(
|
|
992
|
-
JSON.stringify({
|
|
993
|
-
jsonrpc: "2.0",
|
|
994
|
-
error: { code: -32000, message: "Not found" },
|
|
995
|
-
id: null,
|
|
996
|
-
}),
|
|
997
|
-
);
|
|
1159
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Not found" }, id: null }));
|
|
998
1160
|
} catch (err) {
|
|
999
1161
|
debugLog("[ctxce] Unexpected error in HTTP MCP server: " + String(err));
|
|
1000
1162
|
if (!res.headersSent) {
|
|
1001
1163
|
res.statusCode = 500;
|
|
1002
1164
|
res.setHeader("Content-Type", "application/json");
|
|
1003
|
-
res.end(
|
|
1004
|
-
JSON.stringify({
|
|
1005
|
-
jsonrpc: "2.0",
|
|
1006
|
-
error: { code: -32603, message: "Internal server error" },
|
|
1007
|
-
id: null,
|
|
1008
|
-
}),
|
|
1009
|
-
);
|
|
1165
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }));
|
|
1010
1166
|
}
|
|
1011
1167
|
}
|
|
1012
1168
|
});
|
|
1013
1169
|
|
|
1014
1170
|
// Bind to 127.0.0.1 only (localhost) for local-only OAuth security
|
|
1015
1171
|
httpServer.listen(port, '127.0.0.1', () => {
|
|
1016
|
-
debugLog(`[ctxce] HTTP MCP bridge listening on 127.0.0.1:${port}`);
|
|
1172
|
+
debugLog(`[ctxce] HTTP MCP bridge listening on 127.0.0.1:${port} (multi-session enabled)`);
|
|
1017
1173
|
});
|
|
1018
1174
|
|
|
1019
1175
|
let shuttingDown = false;
|
|
1020
1176
|
const shutdown = (signal) => {
|
|
1021
1177
|
if (shuttingDown) return;
|
|
1022
1178
|
shuttingDown = true;
|
|
1023
|
-
debugLog(`[ctxce] Received ${signal}; closing HTTP server
|
|
1179
|
+
debugLog(`[ctxce] Received ${signal}; closing HTTP server and ${sessions.size} active session(s).`);
|
|
1180
|
+
clearInterval(cleanupInterval);
|
|
1181
|
+
// Close all active sessions
|
|
1182
|
+
for (const [, session] of sessions) {
|
|
1183
|
+
session.transport.close().catch(() => {});
|
|
1184
|
+
session.server.close().catch(() => {});
|
|
1185
|
+
}
|
|
1186
|
+
sessions.clear();
|
|
1024
1187
|
httpServer.close(() => {
|
|
1025
1188
|
debugLog("[ctxce] HTTP server closed.");
|
|
1026
1189
|
process.exit(0);
|
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`.
|