@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.
@@ -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.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}; missing or invalid token/session, marking local session as expired.`,
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
- config && typeof config.default_collection === "string"
450
- ? config.default_collection
451
- : null;
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 appears expired; please run `ctxce auth login` again.");
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( // TODO: marked as depreciated
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
- const server = await createBridgeServer(options);
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
- const transport = new StreamableHTTPServerTransport({
918
- sessionIdGenerator: undefined,
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
- await server.connect(transport);
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
- // Check Bearer token for MCP endpoint (accept /mcp and /mcp/ for compatibility)
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 sessionId = oauthHandler.lookupToken(bearerToken);
1004
- if (!sessionId) {
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 (waiting for in-flight requests).`);
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);
@@ -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.innerHTML = '<p class="error">' + err.message + '</p>';
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>