cbrowser 19.3.7 → 19.3.9

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.
Files changed (159) hide show
  1. package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
  2. package/dist/analysis/accessibility-empathy.js +10 -1
  3. package/dist/analysis/accessibility-empathy.js.map +1 -1
  4. package/dist/analysis/agent-ready-audit.d.ts +9 -0
  5. package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
  6. package/dist/analysis/agent-ready-audit.js +79 -6
  7. package/dist/analysis/agent-ready-audit.js.map +1 -1
  8. package/dist/analysis/bug-hunter.d.ts.map +1 -1
  9. package/dist/analysis/bug-hunter.js +27 -10
  10. package/dist/analysis/bug-hunter.js.map +1 -1
  11. package/dist/analysis/chaos-testing.d.ts.map +1 -1
  12. package/dist/analysis/chaos-testing.js +12 -3
  13. package/dist/analysis/chaos-testing.js.map +1 -1
  14. package/dist/analysis/competitive-benchmark.d.ts +50 -7
  15. package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
  16. package/dist/analysis/competitive-benchmark.js +226 -106
  17. package/dist/analysis/competitive-benchmark.js.map +1 -1
  18. package/dist/analysis/page-understanding.d.ts +2 -0
  19. package/dist/analysis/page-understanding.d.ts.map +1 -1
  20. package/dist/analysis/page-understanding.js +104 -17
  21. package/dist/analysis/page-understanding.js.map +1 -1
  22. package/dist/artifact-store.d.ts +24 -2
  23. package/dist/artifact-store.d.ts.map +1 -1
  24. package/dist/artifact-store.js +34 -4
  25. package/dist/artifact-store.js.map +1 -1
  26. package/dist/browser/session-manager.d.ts.map +1 -1
  27. package/dist/browser/session-manager.js +3 -0
  28. package/dist/browser/session-manager.js.map +1 -1
  29. package/dist/browser/site-profile-manager.d.ts +17 -1
  30. package/dist/browser/site-profile-manager.d.ts.map +1 -1
  31. package/dist/browser/site-profile-manager.js +49 -12
  32. package/dist/browser/site-profile-manager.js.map +1 -1
  33. package/dist/browser.d.ts +58 -1
  34. package/dist/browser.d.ts.map +1 -1
  35. package/dist/browser.js +377 -50
  36. package/dist/browser.js.map +1 -1
  37. package/dist/charge-scope.d.ts.map +1 -1
  38. package/dist/charge-scope.js +6 -4
  39. package/dist/charge-scope.js.map +1 -1
  40. package/dist/cli.js +3 -0
  41. package/dist/cli.js.map +1 -1
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +8 -2
  44. package/dist/config.js.map +1 -1
  45. package/dist/mcp-server-remote.d.ts +27 -1
  46. package/dist/mcp-server-remote.d.ts.map +1 -1
  47. package/dist/mcp-server-remote.js +160 -87
  48. package/dist/mcp-server-remote.js.map +1 -1
  49. package/dist/mcp-server.d.ts.map +1 -1
  50. package/dist/mcp-server.js +4 -0
  51. package/dist/mcp-server.js.map +1 -1
  52. package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -1
  53. package/dist/mcp-tools/base/analysis-tools.js +5 -0
  54. package/dist/mcp-tools/base/analysis-tools.js.map +1 -1
  55. package/dist/mcp-tools/base/audit-tools.d.ts +14 -0
  56. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
  57. package/dist/mcp-tools/base/audit-tools.js +17 -8
  58. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  59. package/dist/mcp-tools/base/browser-state-tools.d.ts.map +1 -1
  60. package/dist/mcp-tools/base/browser-state-tools.js +18 -27
  61. package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
  62. package/dist/mcp-tools/base/capture-tools.d.ts +18 -0
  63. package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
  64. package/dist/mcp-tools/base/capture-tools.js +109 -55
  65. package/dist/mcp-tools/base/capture-tools.js.map +1 -1
  66. package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
  67. package/dist/mcp-tools/base/cognitive-tools.js +43 -16
  68. package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
  69. package/dist/mcp-tools/base/marketing-tools.d.ts.map +1 -1
  70. package/dist/mcp-tools/base/marketing-tools.js +8 -4
  71. package/dist/mcp-tools/base/marketing-tools.js.map +1 -1
  72. package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
  73. package/dist/mcp-tools/base/persona-comparison-tools.js +14 -3
  74. package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
  75. package/dist/mcp-tools/base/remediation-tools.d.ts.map +1 -1
  76. package/dist/mcp-tools/base/remediation-tools.js +10 -2
  77. package/dist/mcp-tools/base/remediation-tools.js.map +1 -1
  78. package/dist/mcp-tools/base/session-tools.d.ts.map +1 -1
  79. package/dist/mcp-tools/base/session-tools.js +2 -0
  80. package/dist/mcp-tools/base/session-tools.js.map +1 -1
  81. package/dist/mcp-tools/base/visual-testing-tools.d.ts +13 -0
  82. package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
  83. package/dist/mcp-tools/base/visual-testing-tools.js +45 -16
  84. package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
  85. package/dist/mcp-tools/persona-creation-tools.d.ts.map +1 -1
  86. package/dist/mcp-tools/persona-creation-tools.js +59 -13
  87. package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
  88. package/dist/mcp-tools/persona-lifecycle-tools.d.ts.map +1 -1
  89. package/dist/mcp-tools/persona-lifecycle-tools.js +4 -2
  90. package/dist/mcp-tools/persona-lifecycle-tools.js.map +1 -1
  91. package/dist/mcp-tools/tier-gate.d.ts +4 -4
  92. package/dist/mcp-tools/tier-gate.d.ts.map +1 -1
  93. package/dist/mcp-tools/tier-gate.js +41 -19
  94. package/dist/mcp-tools/tier-gate.js.map +1 -1
  95. package/dist/mcp-tools/tool-categories.d.ts +11 -0
  96. package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
  97. package/dist/mcp-tools/tool-categories.js +27 -1
  98. package/dist/mcp-tools/tool-categories.js.map +1 -1
  99. package/dist/mcp-tools/ui-resources.d.ts.map +1 -1
  100. package/dist/mcp-tools/ui-resources.js +32 -10
  101. package/dist/mcp-tools/ui-resources.js.map +1 -1
  102. package/dist/personas.d.ts +14 -0
  103. package/dist/personas.d.ts.map +1 -1
  104. package/dist/personas.js +26 -0
  105. package/dist/personas.js.map +1 -1
  106. package/dist/recording/engine.d.ts.map +1 -1
  107. package/dist/recording/engine.js +2 -1
  108. package/dist/recording/engine.js.map +1 -1
  109. package/dist/remediation/patch-generator.d.ts.map +1 -1
  110. package/dist/remediation/patch-generator.js +7 -1
  111. package/dist/remediation/patch-generator.js.map +1 -1
  112. package/dist/remediation/structured-data.d.ts +73 -1
  113. package/dist/remediation/structured-data.d.ts.map +1 -1
  114. package/dist/remediation/structured-data.js +239 -63
  115. package/dist/remediation/structured-data.js.map +1 -1
  116. package/dist/request-context.d.ts +18 -0
  117. package/dist/request-context.d.ts.map +1 -0
  118. package/dist/request-context.js +56 -0
  119. package/dist/request-context.js.map +1 -0
  120. package/dist/site-model/manager.d.ts +14 -1
  121. package/dist/site-model/manager.d.ts.map +1 -1
  122. package/dist/site-model/manager.js +24 -1
  123. package/dist/site-model/manager.js.map +1 -1
  124. package/dist/types.d.ts +90 -2
  125. package/dist/types.d.ts.map +1 -1
  126. package/dist/types.js.map +1 -1
  127. package/dist/values/index.d.ts +1 -1
  128. package/dist/values/index.d.ts.map +1 -1
  129. package/dist/values/index.js +1 -1
  130. package/dist/values/index.js.map +1 -1
  131. package/dist/values/persona-values.d.ts +16 -0
  132. package/dist/values/persona-values.d.ts.map +1 -1
  133. package/dist/values/persona-values.js +56 -4
  134. package/dist/values/persona-values.js.map +1 -1
  135. package/dist/visual/ab-comparison.d.ts +22 -0
  136. package/dist/visual/ab-comparison.d.ts.map +1 -1
  137. package/dist/visual/ab-comparison.js +36 -9
  138. package/dist/visual/ab-comparison.js.map +1 -1
  139. package/dist/visual/cognitive-transport.d.ts +15 -1
  140. package/dist/visual/cognitive-transport.d.ts.map +1 -1
  141. package/dist/visual/cognitive-transport.js +42 -12
  142. package/dist/visual/cognitive-transport.js.map +1 -1
  143. package/dist/visual/cross-browser.d.ts +8 -0
  144. package/dist/visual/cross-browser.d.ts.map +1 -1
  145. package/dist/visual/cross-browser.js +8 -4
  146. package/dist/visual/cross-browser.js.map +1 -1
  147. package/dist/visual/distance-metrics.d.ts +11 -2
  148. package/dist/visual/distance-metrics.d.ts.map +1 -1
  149. package/dist/visual/distance-metrics.js +44 -5
  150. package/dist/visual/distance-metrics.js.map +1 -1
  151. package/dist/visual/regression.d.ts +61 -0
  152. package/dist/visual/regression.d.ts.map +1 -1
  153. package/dist/visual/regression.js +123 -50
  154. package/dist/visual/regression.js.map +1 -1
  155. package/dist/visual/responsive.d.ts +8 -0
  156. package/dist/visual/responsive.d.ts.map +1 -1
  157. package/dist/visual/responsive.js +95 -17
  158. package/dist/visual/responsive.js.map +1 -1
  159. package/package.json +1 -1
@@ -40,16 +40,18 @@ import { createRemoteJWKSet, jwtVerify } from "jose";
40
40
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
41
41
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
42
42
  import { CBrowser } from "./browser.js";
43
- import { ensureDirectories } from "./config.js";
43
+ import { ensureDirectories, getDataDir } from "./config.js";
44
44
  // Version from package.json - single source of truth
45
45
  import { VERSION } from "./version.js";
46
46
  // Modular MCP tools (v17.5.0)
47
47
  // getActiveKeyHash is deliberately no longer imported: billing identity is per-request
48
- // (see resolveRequestKeyHash). setActiveKeyHash stays for the usage-logging path, which
49
- // is per-session rather than per-charge.
48
+ // (see resolveRequestKeyHash). setActiveKeyHash, setActiveTier and setSessionApiKey
49
+ // write the calling request's own identity context (request-context.ts), which every
50
+ // HTTP request enters at createServer below. (sec-identity, 2026-10-10)
50
51
  import { registerAllPublicTools, setRemoteMode, setHostedMode, setActiveTier as setTierGate, setActiveKeyHash, isToolAccessible, upgradePrompt } from "./mcp-tools/index.js";
51
52
  import { withPersonaScope, scopedAccountId } from "./persona-scope.js";
52
53
  import { withChargeScope } from "./charge-scope.js";
54
+ import { withRequestIdentity } from "./request-context.js";
53
55
  /**
54
56
  * keyHash -> accountId, so a request can be scoped to the tenant that owns it.
55
57
  *
@@ -76,6 +78,8 @@ export async function resolveRequestAccountId(req) {
76
78
  const after = accountCache.get(keyHash);
77
79
  return after && after.expiresAt > Date.now() ? after.accountId : null;
78
80
  }
81
+ import { TOOL_CATEGORIES, categorizedToolNames } from "./mcp-tools/tool-categories.js";
82
+ import { BUILTIN_PERSONAS, ACCESSIBILITY_PERSONAS, EMOTIONAL_PERSONAS, AGENT_PERSONAS } from "./personas.js";
79
83
  // Shared browser instance (single Chromium process)
80
84
  let browser = null;
81
85
  // Stealth state (enterprise integration)
@@ -157,6 +161,7 @@ export async function getBrowserByToken(token) {
157
161
  browser: sessionBrowser,
158
162
  createdAt: now,
159
163
  lastActivity: now,
164
+ accountId: scopedAccountId(),
160
165
  });
161
166
  const newToken = generateBrowserToken(sessionId);
162
167
  console.log(`[Token] New browser session ${sessionId.substring(0, 8)}... → token ${newToken}`);
@@ -168,6 +173,13 @@ export async function getBrowserByToken(token) {
168
173
  const MAX_CONCURRENT_SESSIONS = parseInt(process.env.MAX_CONCURRENT_SESSIONS || "20", 10);
169
174
  const SESSION_IDLE_TIMEOUT_MS = parseInt(process.env.SESSION_IDLE_TIMEOUT_MS || "300000", 10); // 5 min
170
175
  const SESSION_MEMORY_LIMIT_MB = parseInt(process.env.SESSION_MEMORY_LIMIT_MB || "800", 10); // 800MB per session
176
+ /** Thrown when a session id names a browser another account created. */
177
+ export class SessionOwnedElsewhereError extends Error {
178
+ constructor() {
179
+ super("This MCP session id is not available to this caller. Reconnect to start a new session, or call navigate() without a _browserToken.");
180
+ this.name = "SessionOwnedElsewhereError";
181
+ }
182
+ }
171
183
  // Active sessions: sessionId -> SessionInfo
172
184
  const activeSessions = new Map();
173
185
  // Track recently expired sessions for clear error messaging
@@ -179,7 +191,7 @@ const EXPIRED_SESSION_RETENTION_MS = 600000; // 10 min
179
191
  * Each session gets its own CBrowser with isolated context.
180
192
  * If a session expired, transparently create a new one (low-friction reconnect).
181
193
  */
182
- async function getSessionBrowser(sessionId) {
194
+ export async function getSessionBrowser(sessionId) {
183
195
  // Check if this session was recently expired - auto-recover by creating new session
184
196
  const expiredInfo = expiredSessions.get(sessionId);
185
197
  if (expiredInfo) {
@@ -189,9 +201,16 @@ async function getSessionBrowser(sessionId) {
189
201
  expiredSessions.delete(sessionId);
190
202
  // Fall through to create new session below
191
203
  }
192
- // Check if session already exists
204
+ // Check if session already exists. The id is the client's mcp-session-id
205
+ // header on the /mcp path, so holding it proves nothing: this map is one per
206
+ // process and returned the browser (cookies, logged-in pages) to any caller
207
+ // presenting the id. A session is now reused only by the account scope that
208
+ // created it; unscoped callers keep the old behaviour among themselves.
209
+ // (tenant, 2026-10-10)
193
210
  const existing = activeSessions.get(sessionId);
194
211
  if (existing) {
212
+ if (existing.accountId !== scopedAccountId())
213
+ throw new SessionOwnedElsewhereError();
195
214
  existing.lastActivity = Date.now();
196
215
  return existing.browser;
197
216
  }
@@ -215,6 +234,7 @@ async function getSessionBrowser(sessionId) {
215
234
  browser: sessionBrowser,
216
235
  createdAt: Date.now(),
217
236
  lastActivity: Date.now(),
237
+ accountId: scopedAccountId(),
218
238
  });
219
239
  console.log(`[Session] Created new session ${sessionId.substring(0, 8)}... (${activeSessions.size}/${MAX_CONCURRENT_SESSIONS} active)`);
220
240
  return sessionBrowser;
@@ -437,6 +457,10 @@ async function getBrowser() {
437
457
  headless: true,
438
458
  persistent: true,
439
459
  proxy: proxyConfig,
460
+ // The shared legacy browser belongs to no account, so its files stay
461
+ // in the process data dir even if it is first built inside a scope;
462
+ // the config default is now the caller's directory. (tenant, 2026-10-10)
463
+ dataDir: getDataDir(),
440
464
  });
441
465
  }
442
466
  return browser;
@@ -448,7 +472,20 @@ async function getBrowser() {
448
472
  */
449
473
  export async function getTokenlessBrowser() {
450
474
  const accountId = scopedAccountId();
451
- return accountId === undefined ? getBrowser() : getSessionBrowser(`account:${accountId}`);
475
+ return accountId === undefined ? getBrowser() : getSessionBrowser(`${ACCOUNT_SESSION_PREFIX}${accountId}`);
476
+ }
477
+ /**
478
+ * Session ids in this namespace name an account's own tokenless browser. On the
479
+ * /mcp path the session id is the client's mcp-session-id header, and account
480
+ * ids are small integers, so 19.3.7 let any caller send "account:2" and drive
481
+ * account 2's browser, or claim the id first and lock the owner out. A client
482
+ * header in this namespace is ignored, as if no session id had been sent.
483
+ * (19.3.8, 2026-10-10)
484
+ */
485
+ export const ACCOUNT_SESSION_PREFIX = "account:";
486
+ export function sessionIdFromHeader(header) {
487
+ const id = Array.isArray(header) ? header[0] : header;
488
+ return id && !id.startsWith(ACCOUNT_SESSION_PREFIX) ? id : undefined;
452
489
  }
453
490
  // Transport storage for stateful sessions
454
491
  const transports = new Map();
@@ -1055,6 +1092,13 @@ function sendUnauthorized(res, message) {
1055
1092
  }),
1056
1093
  }));
1057
1094
  }
1095
+ /**
1096
+ * The tier a request is gated at.
1097
+ * Priority: resolved from API key > CBROWSER_TIER env > null (self-hosted, no gating).
1098
+ */
1099
+ function effectiveTier(resolvedTier) {
1100
+ return resolvedTier ?? process.env.CBROWSER_TIER ?? null;
1101
+ }
1058
1102
  /**
1059
1103
  * Configure all CBrowser tools on an MCP server instance.
1060
1104
  * This is shared between stdio and HTTP transports.
@@ -1072,8 +1116,7 @@ function configureMcpTools(server, customRegisterTools, sessionId, resolvedTier)
1072
1116
  ? () => getSessionBrowser(sessionId)
1073
1117
  : getTokenlessBrowser;
1074
1118
  const context = { getBrowser: sessionGetBrowser, getBrowserByToken };
1075
- // Tier priority: resolved from API key > CBROWSER_TIER env > null (self-hosted, no gating)
1076
- const tier = resolvedTier ?? process.env.CBROWSER_TIER ?? null;
1119
+ const tier = effectiveTier(resolvedTier);
1077
1120
  if (customRegisterTools) {
1078
1121
  // Set tier BEFORE custom registration so createGatedServer uses resolved tier
1079
1122
  setTierGate(tier);
@@ -1084,30 +1127,30 @@ function configureMcpTools(server, customRegisterTools, sessionId, resolvedTier)
1084
1127
  }
1085
1128
  }
1086
1129
  /**
1087
- * Create a configured MCP server instance
1130
+ * Counts this server prints about itself, all derived, never typed.
1088
1131
  *
1089
- * @param customRegisterTools - Optional custom tool registration
1090
- * @param sessionId - Optional session ID for session isolation
1132
+ * Every number below used to be a literal, and they disagreed with each other
1133
+ * and with tools/list: the instructions said "120 tools across 16 categories",
1134
+ * docs://tools-overview said 105 in its description and "120 across 11" in its
1135
+ * body, persona_creation was "(6 tools)" with nine registered, and
1136
+ * docs://personas said 21 while listing 6 of the 10 general personas. Measured
1137
+ * 2026-10-10: tools/list on the enterprise server returned 130 tools, 126 of
1138
+ * them in TOOL_CATEGORIES' 20 categories and 4 in none (B33).
1091
1139
  */
1092
- function createMcpServer(customRegisterTools, sessionId, resolvedTier) {
1093
- const server = new McpServer({
1094
- name: "cbrowser",
1095
- title: "CBrowser — Cognitive Browser Automation",
1096
- version: VERSION,
1097
- description: "AI-powered browser automation with cognitive user simulation. 120 MCP tools for navigation, visual testing, accessibility auditing, persona-based testing, site knowledge learning, and security scanning. Simulates how real humans think, struggle, and give up on your site.",
1098
- websiteUrl: "https://cbrowser.ai",
1099
- icons: [
1100
- { src: "https://cbrowser.ai/icon-192.png", sizes: ["192x192"] },
1101
- { src: "https://cbrowser.ai/icon-512.png", sizes: ["512x512"] },
1102
- { src: "https://cbrowser.ai/logo.svg", sizes: ["1080x1080"] },
1103
- ],
1104
- }, {
1105
- capabilities: {
1106
- tools: { listChanged: true },
1107
- prompts: {},
1108
- resources: {},
1109
- },
1110
- instructions: `CBrowser — cognitive browser automation with 120 tools across 16 categories.
1140
+ function catalogCounts() {
1141
+ return { tools: categorizedToolNames().length, categories: TOOL_CATEGORIES.length };
1142
+ }
1143
+ /** One line per specialist category, counted from TOOL_CATEGORIES. */
1144
+ function specialistCategoryLines() {
1145
+ return TOOL_CATEGORIES
1146
+ .filter((cat) => cat.tier > 1)
1147
+ .map((cat) => `• ${cat.name} (${cat.tools.length} tool${cat.tools.length === 1 ? "" : "s"}) — ${cat.tools.slice(0, 5).join(", ")}${cat.tools.length > 5 ? "..." : ""}`)
1148
+ .join("\n");
1149
+ }
1150
+ /** The server instructions sent on initialize. Exported so the counts can be tested. */
1151
+ export function buildServerInstructions() {
1152
+ const { tools, categories } = catalogCounts();
1153
+ return `CBrowser — cognitive browser automation with ${tools} tools across ${categories} categories.
1111
1154
 
1112
1155
  CORE TOOLS (use these first):
1113
1156
  • navigate, click, fill, scroll, screenshot, extract — browser control
@@ -1120,17 +1163,7 @@ CORE TOOLS (use these first):
1120
1163
  • security_audit — MCP tool injection scanning
1121
1164
 
1122
1165
  SPECIALIST CATEGORIES (use when the task requires them):
1123
- • visual_testing (9 tools) — visual_baseline, visual_regression, cross_browser_test, responsive_test, ab_comparison, transport_map, attention_analysis...
1124
- • cognitive_transport (7 tools) — cognitive_distance, cognitive_coverage, cognitive_interpolate, cognitive_load_estimate, compare_personas...
1125
- • testing (5 tools) — nl_test_inline, nl_test_file, detect_flaky_tests, coverage_map, repair_test
1126
- • site_knowledge (5 tools) — site_model_status, site_model_query, site_profile_list/delete/status
1127
- • bug_hunting (3 tools) — hunt_bugs, chaos_test, competitive_benchmark
1128
- • performance (3 tools) — perf_baseline, perf_regression, list_baselines
1129
- • sessions (4 tools) — save_session, load_session, list_sessions, delete_session
1130
- • marketing (4 tools) — marketing_campaign_create/run/report, marketing_personas_list
1131
- • persona_creation (6 tools) — persona_create_start, questionnaire, from_description
1132
- • enterprise_security (8 tools) — web_security_scan, stealth_enable/disable/check, cloudflare_detect/wait
1133
- • enterprise_marketing (6 tools) — marketing_influence_matrix, lever_analysis, funnel_analyze, audience_discover
1166
+ ${specialistCategoryLines()}
1134
1167
 
1135
1168
  TIPS:
1136
1169
  • Pass _browserToken between tool calls to maintain browser state
@@ -1149,7 +1182,56 @@ SELF-HELP:
1149
1182
  • If you are unsure what a tool does, what a score means, which persona to use, or how tools work together — call question_answer with your question
1150
1183
  • question_answer searches 290+ pages of CBrowser documentation and returns a grounded answer
1151
1184
  • Use it before guessing. Example: question_answer(question: "What is the difference between viewport and full_page scope?")
1152
- • Use it to explain results to the user. Example: question_answer(question: "What does a CTC of 1.6 with readability bottleneck mean?")`,
1185
+ • Use it to explain results to the user. Example: question_answer(question: "What does a CTC of 1.6 with readability bottleneck mean?")`;
1186
+ }
1187
+ /** Body of docs://tools-overview, counted from TOOL_CATEGORIES. */
1188
+ export function buildToolsOverviewText() {
1189
+ const { tools, categories } = catalogCounts();
1190
+ const list = TOOL_CATEGORIES.map((cat) => `${cat.name} (${cat.tools.length})`).join(", ");
1191
+ return `CBrowser provides ${tools} MCP tools across ${categories} categories: ${list}. Visit https://cbrowser.ai/docs/Tools-Overview for the complete reference.`;
1192
+ }
1193
+ /** Body of docs://personas, listed from the persona maps themselves. */
1194
+ export function buildPersonasOverviewText() {
1195
+ const groups = [
1196
+ ["Cognitive", Object.keys(BUILTIN_PERSONAS)],
1197
+ ["Accessibility", Object.keys(ACCESSIBILITY_PERSONAS)],
1198
+ ["Emotional", Object.keys(EMOTIONAL_PERSONAS)],
1199
+ ["AI agent", Object.keys(AGENT_PERSONAS)],
1200
+ ];
1201
+ const total = groups.reduce((n, [, names]) => n + names.length, 0);
1202
+ const body = groups.map(([label, names]) => `${label} (${names.length}): ${names.join(", ")}`).join(". ");
1203
+ return `CBrowser includes ${total} built-in personas: ${body}. Each human persona has 26 cognitive traits; agent personas describe AI agents with agent traits instead. Visit https://cbrowser.ai/docs/Persona-Index for details.`;
1204
+ }
1205
+ /** Number of built-in personas across every map docs://personas lists. */
1206
+ function builtinPersonaCount() {
1207
+ return [BUILTIN_PERSONAS, ACCESSIBILITY_PERSONAS, EMOTIONAL_PERSONAS, AGENT_PERSONAS]
1208
+ .reduce((n, map) => n + Object.keys(map).length, 0);
1209
+ }
1210
+ /**
1211
+ * Create a configured MCP server instance
1212
+ *
1213
+ * @param customRegisterTools - Optional custom tool registration
1214
+ * @param sessionId - Optional session ID for session isolation
1215
+ */
1216
+ function createMcpServer(customRegisterTools, sessionId, resolvedTier) {
1217
+ const server = new McpServer({
1218
+ name: "cbrowser",
1219
+ title: "CBrowser — Cognitive Browser Automation",
1220
+ version: VERSION,
1221
+ description: `AI-powered browser automation with cognitive user simulation. ${catalogCounts().tools} MCP tools for navigation, visual testing, accessibility auditing, persona-based testing, site knowledge learning, and security scanning. Simulates how real humans think, struggle, and give up on your site.`,
1222
+ websiteUrl: "https://cbrowser.ai",
1223
+ icons: [
1224
+ { src: "https://cbrowser.ai/icon-192.png", sizes: ["192x192"] },
1225
+ { src: "https://cbrowser.ai/icon-512.png", sizes: ["512x512"] },
1226
+ { src: "https://cbrowser.ai/logo.svg", sizes: ["1080x1080"] },
1227
+ ],
1228
+ }, {
1229
+ capabilities: {
1230
+ tools: { listChanged: true },
1231
+ prompts: {},
1232
+ resources: {},
1233
+ },
1234
+ instructions: buildServerInstructions(),
1153
1235
  });
1154
1236
  // Register prompts for common workflows
1155
1237
  server.prompt("audit-site", "Run a comprehensive site audit with empathy, agent-readiness, and visual baseline", async () => ({
@@ -1181,17 +1263,17 @@ SELF-HELP:
1181
1263
  }));
1182
1264
  registerUiResources(server);
1183
1265
  // Register resources
1184
- server.resource("docs://tools-overview", "docs://tools-overview", { description: "Overview of all 105 CBrowser MCP tools organized by category", mimeType: "text/plain" }, async () => ({
1266
+ server.resource("docs://tools-overview", "docs://tools-overview", { description: `Overview of all ${catalogCounts().tools} CBrowser MCP tools organized by category`, mimeType: "text/plain" }, async () => ({
1185
1267
  contents: [{
1186
1268
  uri: "docs://tools-overview",
1187
- text: "CBrowser provides 120 MCP tools across 11 categories: Browser Automation, Visual Testing, Cognitive Journeys, Site Knowledge, Accessibility, Persona System, Testing, Performance, Security, Session Management, and Marketing. Visit https://cbrowser.ai/docs/Tools-Overview for the complete reference.",
1269
+ text: buildToolsOverviewText(),
1188
1270
  mimeType: "text/plain",
1189
1271
  }],
1190
1272
  }));
1191
- server.resource("docs://personas", "docs://personas", { description: "List of 21 built-in personas with cognitive traits", mimeType: "text/plain" }, async () => ({
1273
+ server.resource("docs://personas", "docs://personas", { description: `List of ${builtinPersonaCount()} built-in personas`, mimeType: "text/plain" }, async () => ({
1192
1274
  contents: [{
1193
1275
  uri: "docs://personas",
1194
- text: "CBrowser includes 21 personas: Cognitive (6): power-user, first-timer, mobile-user, elderly-user, impatient-user, screen-reader-user. Accessibility (11): motor-impairment-tremor, low-vision-magnified, cognitive-adhd, dyslexic-user, deaf-user, elderly-low-vision, color-blind-deuteranopia, autism-spectrum, intellectual-disability, aphasia-receptive, dyscalculia. Emotional (4): anxious-user, confident-user, emotional-user, stoic-user. Each persona has 26 cognitive traits. Visit https://cbrowser.ai/docs/Persona-Index for details.",
1276
+ text: buildPersonasOverviewText(),
1195
1277
  mimeType: "text/plain",
1196
1278
  }],
1197
1279
  }));
@@ -1532,7 +1614,13 @@ export async function startRemoteMcpServer(options) {
1532
1614
  console.log(` - Enterprise: unlimited`);
1533
1615
  }
1534
1616
  console.log(`Listening on ${host}:${port}`);
1535
- const httpServer = createServer(async (req, res) => {
1617
+ // Every HTTP request runs in its own identity context, so the key hash, tier
1618
+ // and API key the /mcp branch sets for it belong to it alone. They were module
1619
+ // globals, set while the request's server was built and read when its tool
1620
+ // ran, several awaits later; an overlapping request from another account
1621
+ // overwrote them in between. The context starts as an unauthenticated caller
1622
+ // at the deployment's default tier. (sec-identity, 2026-10-10)
1623
+ const httpServer = createServer((req, res) => withRequestIdentity({ keyHash: null, tier: effectiveTier(null), apiKey: undefined }, async () => {
1536
1624
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
1537
1625
  // Security headers (defense in depth)
1538
1626
  res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
@@ -1874,11 +1962,14 @@ if (urlParams.get('error')) {
1874
1962
  }
1875
1963
  // llms.txt - AI-readable documentation (WebMCP standard)
1876
1964
  if (url.pathname === "/llms.txt" || url.pathname === "/.well-known/llms.txt") {
1965
+ // Count and category list derived from TOOL_CATEGORIES. The hand-written
1966
+ // block said 91 tools, and its per-category numbers did not match the
1967
+ // names it listed ("Visual Testing (6)" named five). (B33, 2026-10-10)
1877
1968
  res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1878
1969
  res.end(`# CBrowser MCP Server
1879
1970
  # AI-powered browser automation with cognitive user simulation
1880
1971
 
1881
- > CBrowser is an MCP server that provides 91 tools for browser automation,
1972
+ > CBrowser is an MCP server that provides ${catalogCounts().tools} tools for browser automation,
1882
1973
  > visual testing, cognitive user simulation, and accessibility auditing.
1883
1974
  > It simulates how real humans think, struggle, and give up on websites.
1884
1975
 
@@ -1901,17 +1992,8 @@ POST /mcp - Model Context Protocol endpoint (JSON-RPC 2.0 over SSE)
1901
1992
  - API Key authentication for programmatic access
1902
1993
  - Demo server (demo.cbrowser.ai): Rate-limited, no auth required
1903
1994
 
1904
- ## Tool Categories (91 tools)
1905
- - Navigation (1): navigate
1906
- - Interaction (5): click, smart_click, fill, scroll, press_key
1907
- - Analysis (4): analyze_page, extract, find_element_by_intent, assert
1908
- - Visual Testing (6): visual_baseline, visual_regression, cross_browser_test, responsive_test, ab_comparison
1909
- - Cognitive (3): cognitive_journey_init, cognitive_journey_update_state, list_cognitive_personas
1910
- - Personas (10): compare_personas_init, persona_create, persona_traits_list, etc.
1911
- - Testing (5): nl_test_file, nl_test_inline, repair_test, detect_flaky_tests, coverage_map
1912
- - Audit (4): agent_ready_audit, competitive_benchmark, empathy_audit, webmcp_ready_audit
1913
- - Session (4): save_session, load_session, list_sessions, delete_session
1914
- - Browser (4): browser_health, browser_recover, reset_browser, status
1995
+ ## Tool Categories (${catalogCounts().tools} tools)
1996
+ ${TOOL_CATEGORIES.map((cat) => `- ${cat.name} (${cat.tools.length}): ${cat.tools.join(", ")}`).join("\n")}
1915
1997
 
1916
1998
  ## Links
1917
1999
  - Documentation: https://cbrowser.ai/docs
@@ -1958,8 +2040,8 @@ ${VERSION}
1958
2040
  }
1959
2041
  }</code></pre>
1960
2042
 
1961
- <h2>Available Tools (91)</h2>
1962
- <p>This server provides 91 MCP tools for browser automation, testing, and analysis.</p>
2043
+ <h2>Available Tools (${catalogCounts().tools})</h2>
2044
+ <p>This server provides ${catalogCounts().tools} MCP tools for browser automation, testing, and analysis.</p>
1963
2045
  <ul>
1964
2046
  <li><strong>Navigation:</strong> navigate, screenshot</li>
1965
2047
  <li><strong>Interaction:</strong> click, smart_click, fill, scroll, press_key</li>
@@ -2195,7 +2277,7 @@ ${VERSION}
2195
2277
  // Get or create session
2196
2278
  // v18.14.0: Use client's mcp-session-id for browser session correlation
2197
2279
  // This fixes DOM interaction desync where navigate worked but other tools saw blank page
2198
- const existingSessionId = req.headers["mcp-session-id"];
2280
+ const existingSessionId = sessionIdFromHeader(req.headers["mcp-session-id"]);
2199
2281
  let transport;
2200
2282
  let browserSessionId;
2201
2283
  // First, determine the browser session ID (independent of transport mode)
@@ -2224,6 +2306,21 @@ ${VERSION}
2224
2306
  // Log session handling for debugging (truncate ID for readability)
2225
2307
  const shortId = browserSessionId.substring(0, 8);
2226
2308
  console.log(`[Session] ${sessionSource}: ${shortId}... (active: ${activeSessions.size})`);
2309
+ // This request's caller, written into its own identity context (entered at
2310
+ // createServer above). Set before the transport branch so a reused
2311
+ // stateful transport gets it too: that path used to run on whatever the
2312
+ // last request to build a server had left in the globals. Building a new
2313
+ // server below sets the tier again during registration, as it always did.
2314
+ // (sec-identity, 2026-10-10)
2315
+ const requestApiKey = extractApiKey(req);
2316
+ const resolvedTier = requestApiKey ? await resolveApiKeyTier(requestApiKey) : null;
2317
+ // Key hash for usage tracking: a cbk_ key's own hash, or the key an OAuth
2318
+ // token was issued for; null otherwise. The same derivation as billing.
2319
+ setActiveKeyHash(await resolveRequestKeyHash(req));
2320
+ // Raw key for account-scoped features (custom personas, etc.)
2321
+ const { setSessionApiKey } = await import("./mcp-tools/base/cognitive-tools.js");
2322
+ setSessionApiKey(requestApiKey || undefined);
2323
+ setTierGate(effectiveTier(resolvedTier));
2227
2324
  // Now handle transport (stateful mode caches transport, stateless creates new each time)
2228
2325
  const hasExistingTransport = existingSessionId ? transports.has(existingSessionId) : false;
2229
2326
  if (sessionMode === "stateful" && existingSessionId && hasExistingTransport) {
@@ -2237,30 +2334,6 @@ ${VERSION}
2237
2334
  transport = new StreamableHTTPServerTransport({
2238
2335
  sessionIdGenerator: sessionMode === "stateful" ? () => browserSessionId : undefined,
2239
2336
  });
2240
- // Resolve pricing tier from API key (cbk_ keys → CMS lookup)
2241
- const requestApiKey = extractApiKey(req);
2242
- const resolvedTier = requestApiKey ? await resolveApiKeyTier(requestApiKey) : null;
2243
- // Set key hash for usage tracking
2244
- if (requestApiKey?.startsWith("cbk_")) {
2245
- const { createHash } = await import("crypto");
2246
- setActiveKeyHash(createHash("sha256").update(requestApiKey).digest("hex"));
2247
- }
2248
- else if (requestApiKey) {
2249
- // Check token→keyHash map (OAuth tokens that aren't raw cbk_ keys)
2250
- const mapped = tokenToKeyHash.get(requestApiKey);
2251
- if (mapped && mapped.expiresAt > Date.now()) {
2252
- setActiveKeyHash(mapped.keyHash);
2253
- }
2254
- else {
2255
- setActiveKeyHash(null);
2256
- }
2257
- }
2258
- else {
2259
- setActiveKeyHash(null);
2260
- }
2261
- // Set session API key for account-scoped features (custom personas, etc.)
2262
- const { setSessionApiKey } = await import("./mcp-tools/base/cognitive-tools.js");
2263
- setSessionApiKey(requestApiKey || undefined);
2264
2337
  // Create and connect server with session isolation and tier gating
2265
2338
  const server = createMcpServer(options?.registerTools, browserSessionId, resolvedTier);
2266
2339
  // Allow extension with additional tools (for Enterprise)
@@ -2318,7 +2391,7 @@ ${VERSION}
2318
2391
  // 404 for other paths
2319
2392
  res.writeHead(404, { "Content-Type": "application/json" });
2320
2393
  res.end(JSON.stringify({ error: "Not found" }));
2321
- });
2394
+ }));
2322
2395
  httpServer.listen(port, host, () => {
2323
2396
  void (async () => {
2324
2397
  console.log(`\nCognitive Browser Remote MCP Server running at http://${host}:${port}`);