@context-engine-bridge/context-engine-mcp-bridge 0.0.18 → 0.0.20
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/cli.js +22 -5
- package/src/mcpServer.js +157 -96
- package/src/oauthHandler.js +5 -1
|
@@ -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.20",
|
|
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/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ export async function runCli() {
|
|
|
23
23
|
let indexerUrl = process.env.CTXCE_INDEXER_URL || "http://localhost:8003/mcp";
|
|
24
24
|
let memoryUrl = process.env.CTXCE_MEMORY_URL || null;
|
|
25
25
|
let port = Number.parseInt(process.env.CTXCE_HTTP_PORT || "30810", 10) || 30810;
|
|
26
|
+
let collection = null;
|
|
26
27
|
|
|
27
28
|
for (let i = 0; i < args.length; i += 1) {
|
|
28
29
|
const a = args[i];
|
|
@@ -57,13 +58,20 @@ export async function runCli() {
|
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
if (a === "--collection") {
|
|
62
|
+
if (i + 1 < args.length) {
|
|
63
|
+
collection = args[i + 1];
|
|
64
|
+
i += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
// eslint-disable-next-line no-console
|
|
63
71
|
console.error(
|
|
64
|
-
`[ctxce] Starting HTTP MCP bridge: workspace=${workspace}, port=${port}, indexerUrl=${indexerUrl}, memoryUrl=${memoryUrl || "disabled"}`,
|
|
72
|
+
`[ctxce] Starting HTTP MCP bridge: workspace=${workspace}, port=${port}, indexerUrl=${indexerUrl}, memoryUrl=${memoryUrl || "disabled"}, collection=${collection || "auto"}`,
|
|
65
73
|
);
|
|
66
|
-
await runHttpMcpServer({ workspace, indexerUrl, memoryUrl, port });
|
|
74
|
+
await runHttpMcpServer({ workspace, indexerUrl, memoryUrl, port, collection });
|
|
67
75
|
return;
|
|
68
76
|
}
|
|
69
77
|
|
|
@@ -72,10 +80,12 @@ export async function runCli() {
|
|
|
72
80
|
// Supported flags:
|
|
73
81
|
// --workspace / --path : workspace root (default: cwd)
|
|
74
82
|
// --indexer-url : override MCP indexer URL (default env CTXCE_INDEXER_URL or http://localhost:8003/mcp)
|
|
83
|
+
// --collection : collection name to use for MCP calls
|
|
75
84
|
const args = argv.slice(1);
|
|
76
85
|
let workspace = process.cwd();
|
|
77
86
|
let indexerUrl = process.env.CTXCE_INDEXER_URL || "http://localhost:8003/mcp";
|
|
78
87
|
let memoryUrl = process.env.CTXCE_MEMORY_URL || null;
|
|
88
|
+
let collection = null;
|
|
79
89
|
|
|
80
90
|
for (let i = 0; i < args.length; i += 1) {
|
|
81
91
|
const a = args[i];
|
|
@@ -100,13 +110,20 @@ export async function runCli() {
|
|
|
100
110
|
continue;
|
|
101
111
|
}
|
|
102
112
|
}
|
|
113
|
+
if (a === "--collection") {
|
|
114
|
+
if (i + 1 < args.length) {
|
|
115
|
+
collection = args[i + 1];
|
|
116
|
+
i += 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
103
120
|
}
|
|
104
121
|
|
|
105
122
|
// eslint-disable-next-line no-console
|
|
106
123
|
console.error(
|
|
107
|
-
`[ctxce] Starting MCP bridge: workspace=${workspace}, indexerUrl=${indexerUrl}, memoryUrl=${memoryUrl || "disabled"}`,
|
|
124
|
+
`[ctxce] Starting MCP bridge: workspace=${workspace}, indexerUrl=${indexerUrl}, memoryUrl=${memoryUrl || "disabled"}, collection=${collection || "auto"}`,
|
|
108
125
|
);
|
|
109
|
-
await runMcpServer({ workspace, indexerUrl, memoryUrl });
|
|
126
|
+
await runMcpServer({ workspace, indexerUrl, memoryUrl, collection });
|
|
110
127
|
return;
|
|
111
128
|
}
|
|
112
129
|
|
|
@@ -117,7 +134,7 @@ export async function runCli() {
|
|
|
117
134
|
|
|
118
135
|
// eslint-disable-next-line no-console
|
|
119
136
|
console.error(
|
|
120
|
-
`Usage: ${binName} mcp-serve [--workspace <path>] [--indexer-url <url>] [--memory-url <url>] | ${binName} mcp-http-serve [--workspace <path>] [--indexer-url <url>] [--memory-url <url>] [--port <port>] | ${binName} auth <login|status|logout> [--backend-url <url>] [--token <token>] [--username <name> --password <pass>]`,
|
|
137
|
+
`Usage: ${binName} mcp-serve [--workspace <path>] [--indexer-url <url>] [--memory-url <url>] [--collection <name>] | ${binName} mcp-http-serve [--workspace <path>] [--indexer-url <url>] [--memory-url <url>] [--port <port>] [--collection <name>] | ${binName} auth <login|status|logout> [--backend-url <url>] [--token <token>] [--username <name> --password <pass>]`,
|
|
121
138
|
);
|
|
122
139
|
process.exit(1);
|
|
123
140
|
}
|
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";
|
|
@@ -414,18 +415,8 @@ async function fetchBridgeCollectionState({
|
|
|
414
415
|
if (!resp.ok) {
|
|
415
416
|
if (resp.status === 401 || resp.status === 403) {
|
|
416
417
|
debugLog(
|
|
417
|
-
`[ctxce] /bridge/state responded ${resp.status};
|
|
418
|
+
`[ctxce] /bridge/state responded ${resp.status}; session may not be accepted by this endpoint.`,
|
|
418
419
|
);
|
|
419
|
-
if (backendHint) {
|
|
420
|
-
try {
|
|
421
|
-
const entry = loadAuthEntry(backendHint);
|
|
422
|
-
if (entry) {
|
|
423
|
-
saveAuthEntry(backendHint, { ...entry, expiresAt: 1 });
|
|
424
|
-
}
|
|
425
|
-
} catch {
|
|
426
|
-
// ignore failures
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
420
|
return null;
|
|
430
421
|
}
|
|
431
422
|
throw new Error(`bridge/state responded ${resp.status}`);
|
|
@@ -443,12 +434,15 @@ async function createBridgeServer(options) {
|
|
|
443
434
|
const workspace = options.workspace || process.cwd();
|
|
444
435
|
const indexerUrl = options.indexerUrl;
|
|
445
436
|
const memoryUrl = options.memoryUrl;
|
|
437
|
+
const explicitCollection = options.collection;
|
|
446
438
|
|
|
447
439
|
const config = loadConfig(workspace);
|
|
448
440
|
const defaultCollection =
|
|
449
|
-
|
|
450
|
-
?
|
|
451
|
-
:
|
|
441
|
+
explicitCollection && typeof explicitCollection === "string"
|
|
442
|
+
? explicitCollection
|
|
443
|
+
: config && typeof config.default_collection === "string"
|
|
444
|
+
? config.default_collection
|
|
445
|
+
: null;
|
|
452
446
|
const defaultMode =
|
|
453
447
|
config && typeof config.default_mode === "string" ? config.default_mode : null;
|
|
454
448
|
const defaultUnder =
|
|
@@ -502,8 +496,8 @@ async function createBridgeServer(options) {
|
|
|
502
496
|
expiresAt > 0 &&
|
|
503
497
|
expiresAt < Math.floor(Date.now() / 1000)
|
|
504
498
|
) {
|
|
505
|
-
debugLog("[ctxce] Stored auth session
|
|
506
|
-
return
|
|
499
|
+
debugLog("[ctxce] Stored auth session has local expiry in the past; attempting to use it anyway (server will validate).");
|
|
500
|
+
return entry.sessionId;
|
|
507
501
|
}
|
|
508
502
|
return entry.sessionId;
|
|
509
503
|
}
|
|
@@ -707,7 +701,7 @@ async function createBridgeServer(options) {
|
|
|
707
701
|
|
|
708
702
|
await initializeRemoteClients(false);
|
|
709
703
|
|
|
710
|
-
const server = new Server(
|
|
704
|
+
const server = new Server(
|
|
711
705
|
{
|
|
712
706
|
name: "ctx-context-engine-bridge",
|
|
713
707
|
version: "0.0.1",
|
|
@@ -908,17 +902,74 @@ export async function runMcpServer(options) {
|
|
|
908
902
|
}
|
|
909
903
|
|
|
910
904
|
export async function runHttpMcpServer(options) {
|
|
911
|
-
|
|
905
|
+
// Multi-client HTTP mode: each MCP client (Claude, Codex, Kiro, Windsurf,
|
|
906
|
+
// Augment, etc.) gets its own session with a dedicated Server+Transport
|
|
907
|
+
// pair. Sessions are tracked by the Mcp-Session-Id header. This allows
|
|
908
|
+
// multiple IDEs to share the same bridge simultaneously.
|
|
909
|
+
//
|
|
910
|
+
// Flow:
|
|
911
|
+
// 1. Client sends POST with initialize request (no session ID)
|
|
912
|
+
// 2. Bridge creates a new Server+Transport, generates a session ID
|
|
913
|
+
// 3. Response includes Mcp-Session-Id header
|
|
914
|
+
// 4. Client includes Mcp-Session-Id on all subsequent requests
|
|
915
|
+
// 5. Bridge routes to the correct Server+Transport for that session
|
|
916
|
+
//
|
|
917
|
+
// Each session has its own upstream connections (indexer, memory) created
|
|
918
|
+
// by createBridgeServer. Sessions are cleaned up after 30 min of inactivity.
|
|
919
|
+
|
|
912
920
|
const port =
|
|
913
921
|
typeof options.port === "number"
|
|
914
922
|
? options.port
|
|
915
923
|
: Number.parseInt(process.env.CTXCE_HTTP_PORT || "30810", 10) || 30810;
|
|
916
924
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
925
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
926
|
+
// Session management
|
|
927
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
928
|
+
const sessions = new Map(); // sessionId → { server, transport, lastUsed }
|
|
929
|
+
const SESSION_TTL_MS = 30 * 60 * 1000; // 30 minutes inactive
|
|
930
|
+
const MAX_SESSIONS = 20;
|
|
931
|
+
|
|
932
|
+
// Periodic cleanup of stale sessions
|
|
933
|
+
const cleanupInterval = setInterval(() => {
|
|
934
|
+
const now = Date.now();
|
|
935
|
+
for (const [sid, session] of sessions) {
|
|
936
|
+
if (now - session.lastUsed > SESSION_TTL_MS) {
|
|
937
|
+
debugLog(`[ctxce] Cleaning up stale MCP session: ${sid}`);
|
|
938
|
+
session.transport.close().catch(() => {});
|
|
939
|
+
session.server.close().catch(() => {});
|
|
940
|
+
sessions.delete(sid);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}, 60_000);
|
|
944
|
+
cleanupInterval.unref();
|
|
945
|
+
|
|
946
|
+
function evictOldestSession() {
|
|
947
|
+
if (sessions.size < MAX_SESSIONS) return;
|
|
948
|
+
let oldest = null;
|
|
949
|
+
let oldestTime = Infinity;
|
|
950
|
+
for (const [sid, s] of sessions) {
|
|
951
|
+
if (s.lastUsed < oldestTime) {
|
|
952
|
+
oldest = sid;
|
|
953
|
+
oldestTime = s.lastUsed;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (oldest) {
|
|
957
|
+
const s = sessions.get(oldest);
|
|
958
|
+
sessions.delete(oldest);
|
|
959
|
+
s.transport.close().catch(() => {});
|
|
960
|
+
s.server.close().catch(() => {});
|
|
961
|
+
debugLog(`[ctxce] Evicted oldest MCP session: ${oldest}`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
920
964
|
|
|
921
|
-
|
|
965
|
+
// Pre-warm: validate upstream connectivity (best-effort)
|
|
966
|
+
try {
|
|
967
|
+
const warmupServer = await createBridgeServer(options);
|
|
968
|
+
await warmupServer.close();
|
|
969
|
+
debugLog("[ctxce] HTTP bridge validated upstream connectivity");
|
|
970
|
+
} catch (err) {
|
|
971
|
+
debugLog("[ctxce] HTTP bridge warmup failed (non-fatal): " + String(err));
|
|
972
|
+
}
|
|
922
973
|
|
|
923
974
|
// Build issuer URL for OAuth
|
|
924
975
|
// Note: Local-only bridge uses 127.0.0.1. For remote access, this would need to be configurable.
|
|
@@ -969,56 +1020,57 @@ export async function runHttpMcpServer(options) {
|
|
|
969
1020
|
// MCP Endpoint
|
|
970
1021
|
// ================================================================
|
|
971
1022
|
|
|
972
|
-
//
|
|
1023
|
+
// Accept /mcp and /mcp/ for compatibility
|
|
973
1024
|
if (parsedUrl.pathname === "/mcp" || parsedUrl.pathname === "/mcp/") {
|
|
1025
|
+
// ── Bearer token auth (permissive: validate if provided, don't require) ──
|
|
974
1026
|
const authHeader = req.headers["authorization"] || "";
|
|
975
1027
|
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
1028
|
if (bearerToken && oauthHandler.hasTokenStore()) {
|
|
1003
|
-
const
|
|
1004
|
-
if (!
|
|
1005
|
-
// Token provided but invalid - reject
|
|
1029
|
+
const oauthSessionId = oauthHandler.lookupToken(bearerToken);
|
|
1030
|
+
if (!oauthSessionId) {
|
|
1006
1031
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1007
1032
|
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Invalid or expired bearer token" }, id: null }));
|
|
1008
1033
|
return;
|
|
1009
1034
|
}
|
|
1010
1035
|
}
|
|
1011
1036
|
|
|
1037
|
+
const mcpSessionId = typeof req.headers["mcp-session-id"] === "string"
|
|
1038
|
+
? req.headers["mcp-session-id"]
|
|
1039
|
+
: undefined;
|
|
1040
|
+
|
|
1041
|
+
// ── GET: SSE streaming for existing session ──
|
|
1042
|
+
if (req.method === "GET") {
|
|
1043
|
+
if (mcpSessionId && sessions.has(mcpSessionId)) {
|
|
1044
|
+
const session = sessions.get(mcpSessionId);
|
|
1045
|
+
session.lastUsed = Date.now();
|
|
1046
|
+
session.transport.handleRequest(req, res).catch((err) => {
|
|
1047
|
+
debugLog("[ctxce] SSE stream error: " + String(err));
|
|
1048
|
+
});
|
|
1049
|
+
} else {
|
|
1050
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1051
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request: No valid session for SSE" }, id: null }));
|
|
1052
|
+
}
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// ── DELETE: terminate session ──
|
|
1057
|
+
if (req.method === "DELETE") {
|
|
1058
|
+
if (mcpSessionId && sessions.has(mcpSessionId)) {
|
|
1059
|
+
const session = sessions.get(mcpSessionId);
|
|
1060
|
+
sessions.delete(mcpSessionId);
|
|
1061
|
+
session.transport.close().catch(() => {});
|
|
1062
|
+
session.server.close().catch(() => {});
|
|
1063
|
+
debugLog(`[ctxce] Session terminated by client: ${mcpSessionId}`);
|
|
1064
|
+
}
|
|
1065
|
+
res.writeHead(200).end();
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// ── POST: MCP JSON-RPC requests ──
|
|
1012
1070
|
if (req.method !== "POST") {
|
|
1013
1071
|
res.statusCode = 405;
|
|
1014
1072
|
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
|
-
);
|
|
1073
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed" }, id: null }));
|
|
1022
1074
|
return;
|
|
1023
1075
|
}
|
|
1024
1076
|
|
|
@@ -1033,13 +1085,7 @@ export async function runHttpMcpServer(options) {
|
|
|
1033
1085
|
req.destroy();
|
|
1034
1086
|
res.statusCode = 413;
|
|
1035
1087
|
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
|
-
);
|
|
1088
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Request body too large" }, id: null }));
|
|
1043
1089
|
}
|
|
1044
1090
|
});
|
|
1045
1091
|
req.on("end", async () => {
|
|
@@ -1051,30 +1097,50 @@ export async function runHttpMcpServer(options) {
|
|
|
1051
1097
|
debugLog("[ctxce] Failed to parse HTTP MCP request body: " + String(err));
|
|
1052
1098
|
res.statusCode = 400;
|
|
1053
1099
|
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
|
-
);
|
|
1100
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32700, message: "Invalid JSON" }, id: null }));
|
|
1061
1101
|
return;
|
|
1062
1102
|
}
|
|
1063
1103
|
|
|
1064
1104
|
try {
|
|
1105
|
+
// ── Route to existing session ──
|
|
1106
|
+
if (mcpSessionId && sessions.has(mcpSessionId)) {
|
|
1107
|
+
const session = sessions.get(mcpSessionId);
|
|
1108
|
+
session.lastUsed = Date.now();
|
|
1109
|
+
await session.transport.handleRequest(req, res, parsed);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// ── Unknown session ID (expired or invalid) ──
|
|
1114
|
+
if (mcpSessionId) {
|
|
1115
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1116
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Session not found. Client must re-initialize." }, id: null }));
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// ── New session (no session ID = first request / initialize) ──
|
|
1121
|
+
evictOldestSession();
|
|
1122
|
+
|
|
1123
|
+
const server = await createBridgeServer(options);
|
|
1124
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1125
|
+
sessionIdGenerator: () => randomUUID(),
|
|
1126
|
+
onsessioninitialized: (newSessionId) => {
|
|
1127
|
+
debugLog(`[ctxce] New MCP session: ${newSessionId} (active: ${sessions.size + 1})`);
|
|
1128
|
+
sessions.set(newSessionId, { server, transport, lastUsed: Date.now() });
|
|
1129
|
+
},
|
|
1130
|
+
onsessionclosed: (closedSessionId) => {
|
|
1131
|
+
debugLog(`[ctxce] MCP session closed by transport: ${closedSessionId}`);
|
|
1132
|
+
sessions.delete(closedSessionId);
|
|
1133
|
+
server.close().catch(() => {});
|
|
1134
|
+
},
|
|
1135
|
+
});
|
|
1136
|
+
await server.connect(transport);
|
|
1065
1137
|
await transport.handleRequest(req, res, parsed);
|
|
1066
1138
|
} catch (err) {
|
|
1067
1139
|
debugLog("[ctxce] Error handling HTTP MCP request: " + String(err));
|
|
1068
1140
|
if (!res.headersSent) {
|
|
1069
1141
|
res.statusCode = 500;
|
|
1070
1142
|
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
|
-
);
|
|
1143
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }));
|
|
1078
1144
|
}
|
|
1079
1145
|
}
|
|
1080
1146
|
});
|
|
@@ -1084,39 +1150,34 @@ export async function runHttpMcpServer(options) {
|
|
|
1084
1150
|
// 404 for everything else
|
|
1085
1151
|
res.statusCode = 404;
|
|
1086
1152
|
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
|
-
);
|
|
1153
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Not found" }, id: null }));
|
|
1094
1154
|
} catch (err) {
|
|
1095
1155
|
debugLog("[ctxce] Unexpected error in HTTP MCP server: " + String(err));
|
|
1096
1156
|
if (!res.headersSent) {
|
|
1097
1157
|
res.statusCode = 500;
|
|
1098
1158
|
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
|
-
);
|
|
1159
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }));
|
|
1106
1160
|
}
|
|
1107
1161
|
}
|
|
1108
1162
|
});
|
|
1109
1163
|
|
|
1110
1164
|
// Bind to 127.0.0.1 only (localhost) for local-only OAuth security
|
|
1111
1165
|
httpServer.listen(port, '127.0.0.1', () => {
|
|
1112
|
-
debugLog(`[ctxce] HTTP MCP bridge listening on 127.0.0.1:${port}`);
|
|
1166
|
+
debugLog(`[ctxce] HTTP MCP bridge listening on 127.0.0.1:${port} (multi-session enabled)`);
|
|
1113
1167
|
});
|
|
1114
1168
|
|
|
1115
1169
|
let shuttingDown = false;
|
|
1116
1170
|
const shutdown = (signal) => {
|
|
1117
1171
|
if (shuttingDown) return;
|
|
1118
1172
|
shuttingDown = true;
|
|
1119
|
-
debugLog(`[ctxce] Received ${signal}; closing HTTP server
|
|
1173
|
+
debugLog(`[ctxce] Received ${signal}; closing HTTP server and ${sessions.size} active session(s).`);
|
|
1174
|
+
clearInterval(cleanupInterval);
|
|
1175
|
+
// Close all active sessions
|
|
1176
|
+
for (const [, session] of sessions) {
|
|
1177
|
+
session.transport.close().catch(() => {});
|
|
1178
|
+
session.server.close().catch(() => {});
|
|
1179
|
+
}
|
|
1180
|
+
sessions.clear();
|
|
1120
1181
|
httpServer.close(() => {
|
|
1121
1182
|
debugLog("[ctxce] HTTP server closed.");
|
|
1122
1183
|
process.exit(0);
|
package/src/oauthHandler.js
CHANGED
|
@@ -277,7 +277,11 @@ export function getLoginPage(redirectUri, clientId, state, codeChallenge, codeCh
|
|
|
277
277
|
throw new Error('No redirect URL');
|
|
278
278
|
}
|
|
279
279
|
} catch (err) {
|
|
280
|
-
result.
|
|
280
|
+
result.textContent = '';
|
|
281
|
+
var p = document.createElement('p');
|
|
282
|
+
p.className = 'error';
|
|
283
|
+
p.textContent = err.message;
|
|
284
|
+
result.appendChild(p);
|
|
281
285
|
}
|
|
282
286
|
});
|
|
283
287
|
</script>
|