@context-engine-bridge/context-engine-mcp-bridge 0.0.18 → 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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "enableAllProjectMcpServers": true,
3
+ "enabledMcpjsonServers": [
4
+ "context-engine"
5
+ ],
6
+ "permissions": {
7
+ "allow": [
8
+ "mcp__context-engine__repo_search"
9
+ ]
10
+ }
11
+ }
@@ -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.18",
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";
@@ -707,7 +708,7 @@ async function createBridgeServer(options) {
707
708
 
708
709
  await initializeRemoteClients(false);
709
710
 
710
- const server = new Server( // TODO: marked as depreciated
711
+ const server = new Server(
711
712
  {
712
713
  name: "ctx-context-engine-bridge",
713
714
  version: "0.0.1",
@@ -908,17 +909,73 @@ export async function runMcpServer(options) {
908
909
  }
909
910
 
910
911
  export async function runHttpMcpServer(options) {
911
- const server = await createBridgeServer(options);
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
+
912
927
  const port =
913
928
  typeof options.port === "number"
914
929
  ? options.port
915
930
  : Number.parseInt(process.env.CTXCE_HTTP_PORT || "30810", 10) || 30810;
916
931
 
917
- const transport = new StreamableHTTPServerTransport({
918
- sessionIdGenerator: undefined,
919
- });
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
+ }
920
971
 
921
- await server.connect(transport);
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
+ }
922
979
 
923
980
  // Build issuer URL for OAuth
924
981
  // Note: Local-only bridge uses 127.0.0.1. For remote access, this would need to be configurable.
@@ -969,56 +1026,57 @@ export async function runHttpMcpServer(options) {
969
1026
  // MCP Endpoint
970
1027
  // ================================================================
971
1028
 
972
- // Check Bearer token for MCP endpoint (accept /mcp and /mcp/ for compatibility)
1029
+ // Accept /mcp and /mcp/ for compatibility
973
1030
  if (parsedUrl.pathname === "/mcp" || parsedUrl.pathname === "/mcp/") {
1031
+ // ── Bearer token auth (permissive: validate if provided, don't require) ──
974
1032
  const authHeader = req.headers["authorization"] || "";
975
1033
  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
1034
  if (bearerToken && oauthHandler.hasTokenStore()) {
1003
- const sessionId = oauthHandler.lookupToken(bearerToken);
1004
- if (!sessionId) {
1005
- // Token provided but invalid - reject
1035
+ const oauthSessionId = oauthHandler.lookupToken(bearerToken);
1036
+ if (!oauthSessionId) {
1006
1037
  res.writeHead(401, { "Content-Type": "application/json" });
1007
1038
  res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Invalid or expired bearer token" }, id: null }));
1008
1039
  return;
1009
1040
  }
1010
1041
  }
1011
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
+ }
1061
+
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
+ }
1074
+
1075
+ // ── POST: MCP JSON-RPC requests ──
1012
1076
  if (req.method !== "POST") {
1013
1077
  res.statusCode = 405;
1014
1078
  res.setHeader("Content-Type", "application/json");
1015
- res.end(
1016
- JSON.stringify({
1017
- jsonrpc: "2.0",
1018
- error: { code: -32000, message: "Method not allowed" },
1019
- id: null,
1020
- }),
1021
- );
1079
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed" }, id: null }));
1022
1080
  return;
1023
1081
  }
1024
1082
 
@@ -1033,13 +1091,7 @@ export async function runHttpMcpServer(options) {
1033
1091
  req.destroy();
1034
1092
  res.statusCode = 413;
1035
1093
  res.setHeader("Content-Type", "application/json");
1036
- res.end(
1037
- JSON.stringify({
1038
- jsonrpc: "2.0",
1039
- error: { code: -32000, message: "Request body too large" },
1040
- id: null,
1041
- }),
1042
- );
1094
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Request body too large" }, id: null }));
1043
1095
  }
1044
1096
  });
1045
1097
  req.on("end", async () => {
@@ -1051,30 +1103,50 @@ export async function runHttpMcpServer(options) {
1051
1103
  debugLog("[ctxce] Failed to parse HTTP MCP request body: " + String(err));
1052
1104
  res.statusCode = 400;
1053
1105
  res.setHeader("Content-Type", "application/json");
1054
- res.end(
1055
- JSON.stringify({
1056
- jsonrpc: "2.0",
1057
- error: { code: -32700, message: "Invalid JSON" },
1058
- id: null,
1059
- }),
1060
- );
1106
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32700, message: "Invalid JSON" }, id: null }));
1061
1107
  return;
1062
1108
  }
1063
1109
 
1064
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);
1065
1143
  await transport.handleRequest(req, res, parsed);
1066
1144
  } catch (err) {
1067
1145
  debugLog("[ctxce] Error handling HTTP MCP request: " + String(err));
1068
1146
  if (!res.headersSent) {
1069
1147
  res.statusCode = 500;
1070
1148
  res.setHeader("Content-Type", "application/json");
1071
- res.end(
1072
- JSON.stringify({
1073
- jsonrpc: "2.0",
1074
- error: { code: -32603, message: "Internal server error" },
1075
- id: null,
1076
- }),
1077
- );
1149
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }));
1078
1150
  }
1079
1151
  }
1080
1152
  });
@@ -1084,39 +1156,34 @@ export async function runHttpMcpServer(options) {
1084
1156
  // 404 for everything else
1085
1157
  res.statusCode = 404;
1086
1158
  res.setHeader("Content-Type", "application/json");
1087
- res.end(
1088
- JSON.stringify({
1089
- jsonrpc: "2.0",
1090
- error: { code: -32000, message: "Not found" },
1091
- id: null,
1092
- }),
1093
- );
1159
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Not found" }, id: null }));
1094
1160
  } catch (err) {
1095
1161
  debugLog("[ctxce] Unexpected error in HTTP MCP server: " + String(err));
1096
1162
  if (!res.headersSent) {
1097
1163
  res.statusCode = 500;
1098
1164
  res.setHeader("Content-Type", "application/json");
1099
- res.end(
1100
- JSON.stringify({
1101
- jsonrpc: "2.0",
1102
- error: { code: -32603, message: "Internal server error" },
1103
- id: null,
1104
- }),
1105
- );
1165
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }));
1106
1166
  }
1107
1167
  }
1108
1168
  });
1109
1169
 
1110
1170
  // Bind to 127.0.0.1 only (localhost) for local-only OAuth security
1111
1171
  httpServer.listen(port, '127.0.0.1', () => {
1112
- 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)`);
1113
1173
  });
1114
1174
 
1115
1175
  let shuttingDown = false;
1116
1176
  const shutdown = (signal) => {
1117
1177
  if (shuttingDown) return;
1118
1178
  shuttingDown = true;
1119
- debugLog(`[ctxce] Received ${signal}; closing HTTP server (waiting for in-flight requests).`);
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();
1120
1187
  httpServer.close(() => {
1121
1188
  debugLog("[ctxce] HTTP server closed.");
1122
1189
  process.exit(0);