@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.
- package/.claude/settings.local.json +11 -0
- package/.sisyphus/ultrawork-state.json +7 -0
- package/package.json +10 -2
- package/src/mcpServer.js +147 -80
|
@@ -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";
|
|
@@ -707,7 +708,7 @@ async function createBridgeServer(options) {
|
|
|
707
708
|
|
|
708
709
|
await initializeRemoteClients(false);
|
|
709
710
|
|
|
710
|
-
const server = new Server(
|
|
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
|
-
|
|
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
|
-
|
|
918
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
1004
|
-
if (!
|
|
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
|
|
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);
|