cbrowser 17.5.2 → 17.6.0

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 (97) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/mcp-server-remote.d.ts +8 -0
  6. package/dist/mcp-server-remote.d.ts.map +1 -1
  7. package/dist/mcp-server-remote.js +94 -2214
  8. package/dist/mcp-server-remote.js.map +1 -1
  9. package/dist/mcp-tools/ask-user-tools.d.ts +12 -0
  10. package/dist/mcp-tools/ask-user-tools.d.ts.map +1 -0
  11. package/dist/mcp-tools/ask-user-tools.js +60 -0
  12. package/dist/mcp-tools/ask-user-tools.js.map +1 -0
  13. package/dist/mcp-tools/base/analysis-tools.d.ts +12 -0
  14. package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -0
  15. package/dist/mcp-tools/base/analysis-tools.js +70 -0
  16. package/dist/mcp-tools/base/analysis-tools.js.map +1 -0
  17. package/dist/mcp-tools/base/assertion-tools.d.ts +12 -0
  18. package/dist/mcp-tools/base/assertion-tools.d.ts.map +1 -0
  19. package/dist/mcp-tools/base/assertion-tools.js +32 -0
  20. package/dist/mcp-tools/base/assertion-tools.js.map +1 -0
  21. package/dist/mcp-tools/base/audit-tools.d.ts +12 -0
  22. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -0
  23. package/dist/mcp-tools/base/audit-tools.js +114 -0
  24. package/dist/mcp-tools/base/audit-tools.js.map +1 -0
  25. package/dist/mcp-tools/base/browser-management-tools.d.ts +12 -0
  26. package/dist/mcp-tools/base/browser-management-tools.d.ts.map +1 -0
  27. package/dist/mcp-tools/base/browser-management-tools.js +69 -0
  28. package/dist/mcp-tools/base/browser-management-tools.js.map +1 -0
  29. package/dist/mcp-tools/base/bug-analysis-tools.d.ts +12 -0
  30. package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -0
  31. package/dist/mcp-tools/base/bug-analysis-tools.js +97 -0
  32. package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -0
  33. package/dist/mcp-tools/base/cognitive-tools.d.ts +12 -0
  34. package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -0
  35. package/dist/mcp-tools/base/cognitive-tools.js +453 -0
  36. package/dist/mcp-tools/base/cognitive-tools.js.map +1 -0
  37. package/dist/mcp-tools/base/extraction-tools.d.ts +12 -0
  38. package/dist/mcp-tools/base/extraction-tools.d.ts.map +1 -0
  39. package/dist/mcp-tools/base/extraction-tools.js +41 -0
  40. package/dist/mcp-tools/base/extraction-tools.js.map +1 -0
  41. package/dist/mcp-tools/base/healing-tools.d.ts +12 -0
  42. package/dist/mcp-tools/base/healing-tools.d.ts.map +1 -0
  43. package/dist/mcp-tools/base/healing-tools.js +24 -0
  44. package/dist/mcp-tools/base/healing-tools.js.map +1 -0
  45. package/dist/mcp-tools/base/index.d.ts +49 -0
  46. package/dist/mcp-tools/base/index.d.ts.map +1 -0
  47. package/dist/mcp-tools/base/index.js +99 -0
  48. package/dist/mcp-tools/base/index.js.map +1 -0
  49. package/dist/mcp-tools/base/interaction-tools.d.ts +12 -0
  50. package/dist/mcp-tools/base/interaction-tools.d.ts.map +1 -0
  51. package/dist/mcp-tools/base/interaction-tools.js +168 -0
  52. package/dist/mcp-tools/base/interaction-tools.js.map +1 -0
  53. package/dist/mcp-tools/base/navigation-tools.d.ts +12 -0
  54. package/dist/mcp-tools/base/navigation-tools.d.ts.map +1 -0
  55. package/dist/mcp-tools/base/navigation-tools.js +33 -0
  56. package/dist/mcp-tools/base/navigation-tools.js.map +1 -0
  57. package/dist/mcp-tools/base/performance-tools.d.ts +12 -0
  58. package/dist/mcp-tools/base/performance-tools.d.ts.map +1 -0
  59. package/dist/mcp-tools/base/performance-tools.js +103 -0
  60. package/dist/mcp-tools/base/performance-tools.js.map +1 -0
  61. package/dist/mcp-tools/base/persona-comparison-tools.d.ts +12 -0
  62. package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -0
  63. package/dist/mcp-tools/base/persona-comparison-tools.js +242 -0
  64. package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -0
  65. package/dist/mcp-tools/base/session-tools.d.ts +12 -0
  66. package/dist/mcp-tools/base/session-tools.d.ts.map +1 -0
  67. package/dist/mcp-tools/base/session-tools.js +67 -0
  68. package/dist/mcp-tools/base/session-tools.js.map +1 -0
  69. package/dist/mcp-tools/base/testing-tools.d.ts +12 -0
  70. package/dist/mcp-tools/base/testing-tools.d.ts.map +1 -0
  71. package/dist/mcp-tools/base/testing-tools.js +199 -0
  72. package/dist/mcp-tools/base/testing-tools.js.map +1 -0
  73. package/dist/mcp-tools/base/values-tools.d.ts +13 -0
  74. package/dist/mcp-tools/base/values-tools.d.ts.map +1 -0
  75. package/dist/mcp-tools/base/values-tools.js +290 -0
  76. package/dist/mcp-tools/base/values-tools.js.map +1 -0
  77. package/dist/mcp-tools/base/visual-testing-tools.d.ts +12 -0
  78. package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -0
  79. package/dist/mcp-tools/base/visual-testing-tools.js +159 -0
  80. package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -0
  81. package/dist/mcp-tools/enterprise-stubs.d.ts +23 -0
  82. package/dist/mcp-tools/enterprise-stubs.d.ts.map +1 -0
  83. package/dist/mcp-tools/enterprise-stubs.js +238 -0
  84. package/dist/mcp-tools/enterprise-stubs.js.map +1 -0
  85. package/dist/mcp-tools/index.d.ts +36 -0
  86. package/dist/mcp-tools/index.d.ts.map +1 -0
  87. package/dist/mcp-tools/index.js +52 -0
  88. package/dist/mcp-tools/index.js.map +1 -0
  89. package/dist/mcp-tools/persona-creation-tools.d.ts +21 -0
  90. package/dist/mcp-tools/persona-creation-tools.d.ts.map +1 -0
  91. package/dist/mcp-tools/persona-creation-tools.js +528 -0
  92. package/dist/mcp-tools/persona-creation-tools.js.map +1 -0
  93. package/dist/mcp-tools/types.d.ts +24 -0
  94. package/dist/mcp-tools/types.d.ts.map +1 -0
  95. package/dist/mcp-tools/types.js +8 -0
  96. package/dist/mcp-tools/types.js.map +1 -0
  97. package/package.json +6 -1
@@ -39,31 +39,15 @@ import { randomUUID } from "node:crypto";
39
39
  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
- import { z } from "zod";
43
42
  import { CBrowser } from "./browser.js";
44
- import { ensureDirectories, getStatusInfo } from "./config.js";
45
- // Visual module imports
46
- import { runVisualRegression, runCrossBrowserTest, runResponsiveTest, runABComparison, crossBrowserDiff, captureVisualBaseline, listVisualBaselines, } from "./visual/index.js";
47
- // Testing module imports
48
- import { runNLTestSuite, parseNLTestSuite, dryRunNLTestSuite, repairTest, detectFlakyTests, generateCoverageMap, } from "./testing/index.js";
49
- // Analysis module imports
50
- import { huntBugs, runChaosTest, comparePersonas, findElementByIntent, runAgentReadyAudit, runCompetitiveBenchmark, runEmpathyAudit, } from "./analysis/index.js";
51
- // Accessibility personas for empathy audit
52
- import { listAccessibilityPersonas, getAccessibilityPersona } from "./personas.js";
53
- // Persona imports for cognitive journey
54
- import { getPersona, getAnyPersona, listPersonas, getCognitiveProfile, createCognitivePersona, } from "./personas.js";
55
- // Import API key check for bridge workflow detection
56
- import { isApiKeyConfigured } from "./cognitive/index.js";
57
- // Performance module imports
58
- import { capturePerformanceBaseline, detectPerformanceRegression, listPerformanceBaselines, } from "./performance/index.js";
59
- // Values system (Schwartz's 10 Universal Values)
60
- import { getPersonaValues, PERSONA_VALUE_PROFILES, rankInfluencePatternsForProfile, INFLUENCE_PATTERNS, } from "./values/index.js";
43
+ import { ensureDirectories } from "./config.js";
61
44
  // Version from package.json - single source of truth
62
45
  import { VERSION } from "./version.js";
46
+ // Modular MCP tools (v17.5.0)
47
+ import { registerAllPublicTools } from "./mcp-tools/index.js";
63
48
  // Shared browser instance
64
49
  let browser = null;
65
50
  // Stealth state (enterprise integration)
66
- const stealthEnforcer = null;
67
51
  const stealthConfig = null;
68
52
  async function getBrowser() {
69
53
  if (!browser) {
@@ -74,141 +58,110 @@ async function getBrowser() {
74
58
  persistent: true,
75
59
  proxy: proxyConfig,
76
60
  });
77
- // If stealth is enabled and we have an enforcer, apply to new pages
78
- if (stealthEnforcer && stealthConfig?.enabled) {
79
- const page = await browser.getPage();
80
- if (page) {
81
- await stealthEnforcer.applyStealthMeasures(page);
82
- if (proxyConfig) {
83
- console.log(`[Stealth] Browser launched with proxy: ${proxyConfig.server.replace(/:[^:@]*@/, ":****@")}`);
84
- }
85
- }
86
- }
87
61
  }
88
62
  return browser;
89
63
  }
90
- // Transport instances by session (for stateful mode)
64
+ // Transport storage for stateful sessions
91
65
  const transports = new Map();
92
- let auth0Config = null;
93
- // Token cache to avoid hitting Auth0 rate limits
66
+ // Token cache for Auth0 validation
94
67
  const tokenCache = new Map();
95
- const TOKEN_CACHE_TTL = 30 * 60 * 1000; // 30 minutes
96
- /**
97
- * Initialize Auth0 configuration from environment variables
98
- */
68
+ const TOKEN_CACHE_MARGIN = 60 * 1000; // 1 minute margin before expiry
99
69
  function getAuth0Config() {
100
70
  const domain = process.env.AUTH0_DOMAIN;
101
71
  const audience = process.env.AUTH0_AUDIENCE;
102
72
  if (!domain || !audience) {
103
73
  return null;
104
74
  }
105
- if (!auth0Config || auth0Config.domain !== domain) {
106
- auth0Config = {
107
- domain,
108
- audience,
109
- clientId: process.env.AUTH0_CLIENT_ID,
110
- jwks: createRemoteJWKSet(new URL(`https://${domain}/.well-known/jwks.json`)),
111
- };
112
- }
113
- return auth0Config;
75
+ return {
76
+ domain,
77
+ audience,
78
+ clientId: process.env.AUTH0_CLIENT_ID,
79
+ };
114
80
  }
115
- /**
116
- * Validate Auth0 token - supports both JWT and opaque tokens with caching
117
- */
118
81
  async function validateAuth0Token(token) {
119
- const config = getAuth0Config();
120
- if (!config) {
82
+ const auth0 = getAuth0Config();
83
+ if (!auth0)
121
84
  return null;
122
- }
123
- // Check cache first (use first 32 chars of token as key for security)
124
- const cacheKey = token.substring(0, 32);
125
- const cached = tokenCache.get(cacheKey);
126
- if (cached && cached.expires > Date.now()) {
85
+ // Check cache first
86
+ const cached = tokenCache.get(token);
87
+ if (cached && cached.expiry > Date.now()) {
127
88
  return cached.payload;
128
89
  }
129
- const tokenParts = token.split('.');
130
- // If it's a proper JWT (3 parts), validate locally
131
- if (tokenParts.length === 3) {
132
- try {
133
- const { payload } = await jwtVerify(token, config.jwks, {
134
- issuer: `https://${config.domain}/`,
135
- audience: config.audience,
136
- });
137
- console.log("JWT validated successfully for subject:", payload.sub);
138
- // Cache the result
139
- tokenCache.set(cacheKey, { payload, expires: Date.now() + TOKEN_CACHE_TTL });
140
- return payload;
141
- }
142
- catch (error) {
143
- console.error("JWT validation failed:", error instanceof Error ? error.message : error);
144
- return null;
145
- }
146
- }
147
- // For opaque/JWE tokens (5 parts), validate via Auth0's userinfo endpoint
148
- console.log("Opaque token detected, validating via Auth0 userinfo...");
149
90
  try {
150
- const response = await fetch(`https://${config.domain}/userinfo`, {
151
- headers: {
152
- Authorization: `Bearer ${token}`,
153
- },
91
+ // Try JWT validation first
92
+ const jwks = createRemoteJWKSet(new URL(`https://${auth0.domain}/.well-known/jwks.json`));
93
+ const { payload } = await jwtVerify(token, jwks, {
94
+ issuer: `https://${auth0.domain}/`,
95
+ audience: auth0.audience,
96
+ });
97
+ // Cache the result
98
+ const exp = payload.exp ? payload.exp * 1000 : Date.now() + 30 * 60 * 1000;
99
+ tokenCache.set(token, {
100
+ payload,
101
+ expiry: exp - TOKEN_CACHE_MARGIN,
154
102
  });
155
- if (response.ok) {
156
- const userinfo = await response.json();
157
- console.log("Token validated via userinfo for:", userinfo.sub || userinfo.email);
158
- // Cache the result
159
- tokenCache.set(cacheKey, { payload: userinfo, expires: Date.now() + TOKEN_CACHE_TTL });
160
- return userinfo;
103
+ return payload;
104
+ }
105
+ catch (jwtError) {
106
+ // If JWT validation fails, try opaque token validation via userinfo
107
+ try {
108
+ const userinfoResponse = await fetch(`https://${auth0.domain}/userinfo`, {
109
+ headers: {
110
+ Authorization: `Bearer ${token}`,
111
+ },
112
+ });
113
+ if (userinfoResponse.ok) {
114
+ const userinfo = await userinfoResponse.json();
115
+ const payload = {
116
+ sub: userinfo.sub,
117
+ email: userinfo.email,
118
+ name: userinfo.name,
119
+ };
120
+ // Cache opaque tokens for 30 minutes
121
+ tokenCache.set(token, {
122
+ payload,
123
+ expiry: Date.now() + 30 * 60 * 1000 - TOKEN_CACHE_MARGIN,
124
+ });
125
+ return payload;
126
+ }
161
127
  }
162
- else {
163
- console.error("Userinfo validation failed:", response.status, await response.text());
164
- return null;
128
+ catch {
129
+ // Opaque token validation also failed
165
130
  }
166
- }
167
- catch (error) {
168
- console.error("Userinfo request failed:", error instanceof Error ? error.message : error);
169
131
  return null;
170
132
  }
171
133
  }
172
- /**
173
- * Get Protected Resource Metadata (RFC 9728)
174
- * This tells OAuth clients where to authenticate
175
- */
176
134
  function getProtectedResourceMetadata() {
177
- const config = getAuth0Config();
178
- if (!config) {
135
+ const auth0 = getAuth0Config();
136
+ if (!auth0)
179
137
  return null;
180
- }
181
- const serverUrl = process.env.MCP_SERVER_URL || `https://localhost:${process.env.PORT || 3000}`;
182
138
  return {
183
- resource: serverUrl,
184
- authorization_servers: [`https://${config.domain}`],
139
+ resource: auth0.audience,
140
+ authorization_servers: [`https://${auth0.domain}`],
185
141
  bearer_methods_supported: ["header"],
186
- scopes_supported: ["openid", "profile", "cbrowser:read", "cbrowser:write"],
187
- resource_documentation: "https://github.com/alexandriashai/cbrowser#readme",
142
+ scopes_supported: ["openid", "profile", "email"],
188
143
  };
189
144
  }
190
- /**
191
- * Get configured API keys from environment
192
- */
193
145
  function getApiKeys() {
194
146
  const singleKey = process.env.MCP_API_KEY;
195
147
  const multipleKeys = process.env.MCP_API_KEYS;
196
148
  if (!singleKey && !multipleKeys) {
197
- return null; // No authentication configured
149
+ return null;
198
150
  }
199
151
  const keys = new Set();
200
152
  if (singleKey) {
201
153
  keys.add(singleKey);
202
154
  }
203
155
  if (multipleKeys) {
204
- multipleKeys.split(",").map(k => k.trim()).filter(k => k).forEach(k => keys.add(k));
156
+ for (const key of multipleKeys.split(",")) {
157
+ const trimmed = key.trim();
158
+ if (trimmed) {
159
+ keys.add(trimmed);
160
+ }
161
+ }
205
162
  }
206
- return keys;
163
+ return keys.size > 0 ? keys : null;
207
164
  }
208
- /**
209
- * Validate API key from request headers
210
- * Supports: Authorization: Bearer <key> or X-API-Key: <key>
211
- */
212
165
  function validateApiKey(req, validKeys) {
213
166
  // Check Authorization header (Bearer token)
214
167
  const authHeader = req.headers.authorization;
@@ -225,42 +178,28 @@ function validateApiKey(req, validKeys) {
225
178
  }
226
179
  return false;
227
180
  }
228
- /**
229
- * Validate authentication - supports both API keys and Auth0 JWT
230
- * Returns: { valid: true } or { valid: false, reason: string }
231
- */
232
181
  async function validateAuth(req, apiKeys, auth0Enabled) {
233
- const authHeader = req.headers.authorization;
234
- // Try API key first (if configured)
235
- if (apiKeys && apiKeys.size > 0) {
236
- if (validateApiKey(req, apiKeys)) {
237
- return { valid: true };
238
- }
182
+ // Try API key first (faster)
183
+ if (apiKeys && validateApiKey(req, apiKeys)) {
184
+ return { valid: true };
239
185
  }
240
- // Try Auth0 JWT (if configured)
241
- if (auth0Enabled && authHeader?.startsWith("Bearer ")) {
242
- const token = authHeader.slice(7);
243
- const payload = await validateAuth0Token(token);
244
- if (payload) {
245
- return { valid: true, user: payload };
186
+ // Try Auth0 if enabled
187
+ if (auth0Enabled) {
188
+ const authHeader = req.headers.authorization;
189
+ if (authHeader?.startsWith("Bearer ")) {
190
+ const token = authHeader.slice(7);
191
+ const payload = await validateAuth0Token(token);
192
+ if (payload) {
193
+ return { valid: true };
194
+ }
246
195
  }
247
- return { valid: false, reason: "Invalid or expired JWT token" };
248
196
  }
249
- // No valid auth found
250
- if (!apiKeys && !auth0Enabled) {
251
- // No auth configured - allow all
252
- return { valid: true };
253
- }
254
- return { valid: false, reason: "Authentication required" };
197
+ return { valid: false, reason: "Invalid or missing authentication" };
255
198
  }
256
- /**
257
- * Send 401 Unauthorized response with proper WWW-Authenticate header
258
- */
259
199
  function sendUnauthorized(res, message) {
260
200
  const auth0 = getAuth0Config();
261
- let wwwAuth = 'Bearer realm="cbrowser-mcp"';
201
+ let wwwAuth = "Bearer";
262
202
  if (auth0) {
263
- // Include Auth0 authorization server info per RFC 9728
264
203
  wwwAuth = `Bearer realm="cbrowser-mcp", authorization_uri="https://${auth0.domain}/authorize", token_uri="https://${auth0.domain}/oauth/token"`;
265
204
  }
266
205
  res.writeHead(401, {
@@ -281,2090 +220,32 @@ function sendUnauthorized(res, message) {
281
220
  * Configure all CBrowser tools on an MCP server instance.
282
221
  * This is shared between stdio and HTTP transports.
283
222
  */
284
- function configureMcpTools(server) {
285
- // =========================================================================
286
- // Navigation Tools
287
- // =========================================================================
288
- server.tool("navigate", "Navigate to a URL and take a screenshot", {
289
- url: z.string().url().describe("The URL to navigate to"),
290
- }, async ({ url }) => {
291
- const b = await getBrowser();
292
- const result = await b.navigate(url);
293
- return {
294
- content: [
295
- {
296
- type: "text",
297
- text: JSON.stringify({
298
- success: true,
299
- url: result.url,
300
- title: result.title,
301
- loadTime: result.loadTime,
302
- screenshot: result.screenshot,
303
- }, null, 2),
304
- },
305
- ],
306
- };
307
- });
308
- // NOTE: cloudflare_detect and cloudflare_wait moved to Enterprise (v16.18.0)
309
- // =========================================================================
310
- // Interaction Tools
311
- // =========================================================================
312
- server.tool("click", "Click an element on the page using text, selector, or description. Use verbose=true for detailed debug info on failure.", {
313
- selector: z.string().describe("Element to click (text content, CSS selector, or description)"),
314
- force: z.boolean().optional().describe("Bypass safety checks for destructive actions"),
315
- verbose: z.boolean().optional().describe("Return available elements and AI suggestions on failure"),
316
- }, async ({ selector, force, verbose }) => {
317
- const b = await getBrowser();
318
- const result = await b.click(selector, { force, verbose });
319
- const response = {
320
- success: result.success,
321
- message: result.message,
322
- screenshot: result.screenshot,
323
- };
324
- if (verbose && !result.success) {
325
- if (result.availableElements)
326
- response.availableElements = result.availableElements;
327
- if (result.aiSuggestion)
328
- response.aiSuggestion = result.aiSuggestion;
329
- if (result.debugScreenshot)
330
- response.debugScreenshot = result.debugScreenshot;
331
- }
332
- return {
333
- content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
334
- };
335
- });
336
- server.tool("smart_click", "Click with auto-retry and self-healing selectors. v11.8.0: Added confidence gating - only reports success if healed selector has >= 60% confidence.", {
337
- selector: z.string().describe("Element to click"),
338
- maxRetries: z.number().optional().default(3).describe("Maximum retry attempts"),
339
- dismissOverlays: z.boolean().optional().default(false).describe("Dismiss overlays before clicking"),
340
- }, async ({ selector, maxRetries, dismissOverlays }) => {
341
- const b = await getBrowser();
342
- const result = await b.smartClick(selector, { maxRetries, dismissOverlays });
343
- return {
344
- content: [
345
- {
346
- type: "text",
347
- text: JSON.stringify({
348
- success: result.success,
349
- attempts: result.attempts.length,
350
- finalSelector: result.finalSelector,
351
- message: result.message,
352
- aiSuggestion: result.aiSuggestion,
353
- // v11.8.0: Confidence gating fields
354
- confidence: result.confidence,
355
- healed: result.healed,
356
- healReason: result.healReason,
357
- }, null, 2),
358
- },
359
- ],
360
- };
361
- });
362
- server.tool("dismiss_overlay", "Detect and dismiss modal overlays (cookie consent, age verification, newsletter popups). Constitutional Yellow zone.", {
363
- type: z.enum(["auto", "cookie", "age-verify", "newsletter", "custom"]).optional().default("auto").describe("Overlay type to detect"),
364
- customSelector: z.string().optional().describe("Custom CSS selector for overlay close button"),
365
- }, async ({ type, customSelector }) => {
366
- const b = await getBrowser();
367
- const result = await b.dismissOverlay({ type, customSelector });
368
- return {
369
- content: [
370
- {
371
- type: "text",
372
- text: JSON.stringify({
373
- dismissed: result.dismissed,
374
- overlaysFound: result.overlaysFound,
375
- overlaysDismissed: result.overlaysDismissed,
376
- details: result.details,
377
- suggestion: result.suggestion,
378
- }, null, 2),
379
- },
380
- ],
381
- };
382
- });
383
- server.tool("fill", "Fill a form field with text. Use verbose=true for detailed debug info on failure.", {
384
- selector: z.string().describe("Input field to fill (name, placeholder, label, or selector)"),
385
- value: z.string().describe("Value to enter"),
386
- verbose: z.boolean().optional().describe("Return available inputs and AI suggestions on failure"),
387
- }, async ({ selector, value, verbose }) => {
388
- const b = await getBrowser();
389
- const result = await b.fill(selector, value, { verbose });
390
- const response = {
391
- success: result.success,
392
- message: result.message,
393
- };
394
- if (verbose && !result.success) {
395
- if (result.availableInputs)
396
- response.availableInputs = result.availableInputs;
397
- if (result.aiSuggestion)
398
- response.aiSuggestion = result.aiSuggestion;
399
- if (result.debugScreenshot)
400
- response.debugScreenshot = result.debugScreenshot;
401
- }
402
- return {
403
- content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
404
- };
405
- });
406
- server.tool("scroll", "Scroll the page in a direction. Use when content might be below the fold or to navigate long pages.", {
407
- direction: z.enum(["down", "up", "top", "bottom"]).default("down").describe("Scroll direction: down (400px), up (400px), top (page start), bottom (page end)"),
408
- amount: z.number().optional().describe("Custom scroll amount in pixels (overrides direction default)"),
409
- }, async ({ direction, amount }) => {
410
- const b = await getBrowser();
411
- const page = await b.getPage();
412
- const scrollAmount = amount || 400;
413
- let scrollPosition = 0;
414
- let maxScroll = 0;
415
- try {
416
- switch (direction) {
417
- case "top":
418
- await page.evaluate(() => window.scrollTo({ top: 0, behavior: "smooth" }));
419
- break;
420
- case "bottom":
421
- await page.evaluate(() => window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }));
422
- break;
423
- case "up":
424
- await page.evaluate((amt) => window.scrollBy({ top: -amt, behavior: "smooth" }), scrollAmount);
425
- break;
426
- case "down":
427
- default:
428
- await page.evaluate((amt) => window.scrollBy({ top: amt, behavior: "smooth" }), scrollAmount);
429
- break;
430
- }
431
- // Wait for scroll animation
432
- await new Promise((resolve) => setTimeout(resolve, 300));
433
- // Get final scroll position
434
- const scrollInfo = await page.evaluate(() => ({
435
- scrollY: window.scrollY,
436
- maxScroll: document.body.scrollHeight - window.innerHeight,
437
- }));
438
- scrollPosition = scrollInfo.scrollY;
439
- maxScroll = scrollInfo.maxScroll;
440
- return {
441
- content: [
442
- {
443
- type: "text",
444
- text: JSON.stringify({
445
- success: true,
446
- direction,
447
- scrollPosition,
448
- maxScroll,
449
- atTop: scrollPosition <= 0,
450
- atBottom: scrollPosition >= maxScroll - 10,
451
- }, null, 2),
452
- },
453
- ],
454
- };
455
- }
456
- catch (error) {
457
- return {
458
- content: [
459
- {
460
- type: "text",
461
- text: JSON.stringify({
462
- success: false,
463
- error: error.message,
464
- }, null, 2),
465
- },
466
- ],
467
- };
468
- }
469
- });
470
- // =========================================================================
471
- // Extraction Tools
472
- // =========================================================================
473
- server.tool("screenshot", "Take a screenshot of the current page", {
474
- path: z.string().optional().describe("Optional path to save the screenshot"),
475
- }, async ({ path }) => {
476
- const b = await getBrowser();
477
- const file = await b.screenshot(path);
478
- return {
479
- content: [
480
- {
481
- type: "text",
482
- text: JSON.stringify({ screenshot: file }, null, 2),
483
- },
484
- ],
485
- };
486
- });
487
- server.tool("extract", "Extract data from the page", {
488
- what: z.enum(["links", "headings", "forms", "images", "text"]).describe("What to extract"),
489
- }, async ({ what }) => {
490
- const b = await getBrowser();
491
- const result = await b.extract(what);
492
- return {
493
- content: [
494
- {
495
- type: "text",
496
- text: JSON.stringify(result.data, null, 2),
497
- },
498
- ],
499
- };
500
- });
501
- // =========================================================================
502
- // Assertion Tools
503
- // =========================================================================
504
- server.tool("assert", "Assert a condition using natural language", {
505
- assertion: z.string().describe("Natural language assertion like \"page contains 'Welcome'\" or \"title is 'Home'\""),
506
- }, async ({ assertion }) => {
507
- const b = await getBrowser();
508
- const result = await b.assert(assertion);
509
- return {
510
- content: [
511
- {
512
- type: "text",
513
- text: JSON.stringify({
514
- passed: result.passed,
515
- message: result.message,
516
- actual: result.actual,
517
- expected: result.expected,
518
- }, null, 2),
519
- },
520
- ],
521
- };
522
- });
523
- // =========================================================================
524
- // Analysis Tools
525
- // =========================================================================
526
- server.tool("analyze_page", "Analyze page structure for forms, buttons, links", {}, async () => {
527
- const b = await getBrowser();
528
- const analysis = await b.analyzePage();
529
- return {
530
- content: [
531
- {
532
- type: "text",
533
- text: JSON.stringify({
534
- title: analysis.title,
535
- forms: analysis.forms.length,
536
- buttons: analysis.buttons.length,
537
- links: analysis.links.length,
538
- hasLogin: analysis.hasLogin,
539
- hasSearch: analysis.hasSearch,
540
- hasNavigation: analysis.hasNavigation,
541
- }, null, 2),
542
- },
543
- ],
544
- };
545
- });
546
- server.tool("generate_tests", "Generate test scenarios for a page", {
547
- url: z.string().url().optional().describe("URL to analyze (uses current page if not provided)"),
548
- }, async ({ url }) => {
549
- const b = await getBrowser();
550
- const result = await b.generateTests(url);
551
- return {
552
- content: [
553
- {
554
- type: "text",
555
- text: JSON.stringify({
556
- testsGenerated: result.tests.length,
557
- tests: result.tests.map(t => ({
558
- name: t.name,
559
- description: t.description,
560
- steps: t.steps.length,
561
- })),
562
- }, null, 2),
563
- },
564
- ],
565
- };
566
- });
567
- // =========================================================================
568
- // Session Tools
569
- // =========================================================================
570
- server.tool("save_session", "Save browser session (cookies, storage) for later use", {
571
- name: z.string().describe("Name for the saved session"),
572
- }, async ({ name }) => {
573
- const b = await getBrowser();
574
- await b.saveSession(name);
575
- return {
576
- content: [
577
- {
578
- type: "text",
579
- text: JSON.stringify({ success: true, sessionName: name }, null, 2),
580
- },
581
- ],
582
- };
583
- });
584
- server.tool("load_session", "Load a previously saved session", {
585
- name: z.string().describe("Name of the session to load"),
586
- }, async ({ name }) => {
587
- const b = await getBrowser();
588
- const result = await b.loadSession(name);
589
- // v11.8.0: Return flat structure, not nested
590
- return {
591
- content: [
592
- {
593
- type: "text",
594
- text: JSON.stringify(result, null, 2),
595
- },
596
- ],
597
- };
598
- });
599
- server.tool("list_sessions", "List all saved sessions with metadata (name, domain, cookies count, localStorage keys, created date, size)", {}, async () => {
600
- const b = await getBrowser();
601
- const sessions = b.listSessionsDetailed();
602
- return {
603
- content: [
604
- {
605
- type: "text",
606
- text: JSON.stringify({ sessions }, null, 2),
607
- },
608
- ],
609
- };
610
- });
611
- server.tool("delete_session", "Delete a saved session by name", {
612
- name: z.string().describe("Name of the session to delete"),
613
- }, async ({ name }) => {
614
- const b = await getBrowser();
615
- const deleted = b.deleteSession(name);
616
- return {
617
- content: [
618
- {
619
- type: "text",
620
- text: JSON.stringify({ success: deleted, name, message: deleted ? `Session '${name}' deleted` : `Session '${name}' not found` }),
621
- },
622
- ],
623
- };
624
- });
625
- // =========================================================================
626
- // Self-Healing Tools
627
- // =========================================================================
628
- server.tool("heal_stats", "Get self-healing selector cache statistics", {}, async () => {
629
- const b = await getBrowser();
630
- const stats = b.getSelectorCacheStats();
631
- return {
632
- content: [
633
- {
634
- type: "text",
635
- text: JSON.stringify(stats, null, 2),
636
- },
637
- ],
638
- };
639
- });
640
- // =========================================================================
641
- // Visual Testing Tools (v7.0.0+)
642
- // =========================================================================
643
- server.tool("visual_baseline", "Capture a visual baseline for a URL", {
644
- url: z.string().url().describe("URL to capture baseline for"),
645
- name: z.string().describe("Name for the baseline"),
646
- }, async ({ url, name }) => {
647
- const result = await captureVisualBaseline(url, name, {});
648
- return {
649
- content: [
650
- {
651
- type: "text",
652
- text: JSON.stringify({
653
- success: true,
654
- name: result.name,
655
- url: result.url,
656
- timestamp: result.timestamp,
657
- }, null, 2),
658
- },
659
- ],
660
- };
661
- });
662
- server.tool("visual_regression", "Run AI visual regression test against a baseline", {
663
- url: z.string().url().describe("URL to test"),
664
- baselineName: z.string().describe("Name of baseline to compare against"),
665
- }, async ({ url, baselineName }) => {
666
- const result = await runVisualRegression(url, baselineName);
667
- return {
668
- content: [
669
- {
670
- type: "text",
671
- text: JSON.stringify({
672
- passed: result.passed,
673
- similarityScore: result.analysis?.similarityScore,
674
- summary: result.analysis?.summary,
675
- changes: result.analysis?.changes?.length || 0,
676
- }, null, 2),
677
- },
678
- ],
679
- };
680
- });
681
- server.tool("cross_browser_test", "Test page rendering across multiple browsers", {
682
- url: z.string().url().describe("URL to test"),
683
- browsers: z.array(z.enum(["chromium", "firefox", "webkit"])).optional().describe("Browsers to test"),
684
- }, async ({ url, browsers }) => {
685
- const result = await runCrossBrowserTest(url, { browsers });
686
- return {
687
- content: [
688
- {
689
- type: "text",
690
- text: JSON.stringify({
691
- url: result.url,
692
- overallStatus: result.overallStatus,
693
- summary: result.summary,
694
- screenshotCount: result.screenshots.length,
695
- comparisonCount: result.comparisons.length,
696
- ...(result.missingBrowsers?.length ? { missingBrowsers: result.missingBrowsers } : {}),
697
- ...(result.availableBrowsers ? { availableBrowsers: result.availableBrowsers } : {}),
698
- ...(result.suggestion ? { suggestion: result.suggestion } : {}),
699
- }, null, 2),
700
- },
701
- ],
702
- };
703
- });
704
- server.tool("cross_browser_diff", "Quick diff of page metrics across browsers", {
705
- url: z.string().url().describe("URL to compare"),
706
- browsers: z.array(z.enum(["chromium", "firefox", "webkit"])).optional().describe("Browsers to compare"),
707
- }, async ({ url, browsers }) => {
708
- const result = await crossBrowserDiff(url, browsers);
709
- return {
710
- content: [
711
- {
712
- type: "text",
713
- text: JSON.stringify({
714
- url: result.url,
715
- browsers: result.browsers,
716
- differences: result.differences,
717
- metrics: result.metrics,
718
- ...(result.missingBrowsers?.length ? { missingBrowsers: result.missingBrowsers } : {}),
719
- ...(result.availableBrowsers ? { availableBrowsers: result.availableBrowsers } : {}),
720
- ...(result.suggestion ? { suggestion: result.suggestion } : {}),
721
- }, null, 2),
722
- },
723
- ],
724
- };
725
- });
726
- server.tool("responsive_test", "Test page across different viewport sizes", {
727
- url: z.string().url().describe("URL to test"),
728
- viewports: z.array(z.string()).optional().describe("Viewport presets (mobile, tablet, desktop, etc.)"),
729
- }, async ({ url, viewports }) => {
730
- const result = await runResponsiveTest(url, { viewports });
731
- return {
732
- content: [
733
- {
734
- type: "text",
735
- text: JSON.stringify({
736
- url: result.url,
737
- overallStatus: result.overallStatus,
738
- summary: result.summary,
739
- viewportsCount: result.screenshots.length,
740
- }, null, 2),
741
- },
742
- ],
743
- };
744
- });
745
- server.tool("ab_comparison", "Compare two URLs visually (staging vs production)", {
746
- urlA: z.string().url().describe("First URL (e.g., staging)"),
747
- urlB: z.string().url().describe("Second URL (e.g., production)"),
748
- labelA: z.string().optional().describe("Label for first URL"),
749
- labelB: z.string().optional().describe("Label for second URL"),
750
- }, async ({ urlA, urlB, labelA, labelB }) => {
751
- const labels = labelA && labelB ? { a: labelA, b: labelB } : undefined;
752
- const result = await runABComparison(urlA, urlB, { labels });
753
- return {
754
- content: [
755
- {
756
- type: "text",
757
- // v11.11.0: Include full differences for structured diff (stress test fix)
758
- text: JSON.stringify({
759
- overallStatus: result.overallStatus,
760
- similarityScore: result.analysis?.similarityScore,
761
- summary: result.summary,
762
- // v11.11.0: Return detailed differences instead of just count
763
- differences: result.differences.slice(0, 10).map(d => ({
764
- type: d.type,
765
- severity: d.severity,
766
- description: d.description,
767
- affectedSide: d.affectedSide,
768
- })),
769
- differenceCount: result.differences.length,
770
- // v11.11.0: Include page structure comparison summary
771
- structureSummary: {
772
- a: {
773
- headings: result.screenshots.a.structure?.headings?.length || 0,
774
- links: result.screenshots.a.structure?.links?.length || 0,
775
- forms: result.screenshots.a.structure?.forms || 0,
776
- buttons: result.screenshots.a.structure?.buttons?.length || 0,
777
- },
778
- b: {
779
- headings: result.screenshots.b.structure?.headings?.length || 0,
780
- links: result.screenshots.b.structure?.links?.length || 0,
781
- forms: result.screenshots.b.structure?.forms || 0,
782
- buttons: result.screenshots.b.structure?.buttons?.length || 0,
783
- },
784
- },
785
- duration: result.duration,
786
- }, null, 2),
787
- },
788
- ],
789
- };
790
- });
791
- // =========================================================================
792
- // Testing Tools (v6.0.0+)
793
- // =========================================================================
794
- server.tool("nl_test_file", "Run natural language test suite from a file. Returns step-level results with enriched error info, partial matches, and suggestions.", {
795
- filepath: z.string().describe("Path to the test file"),
796
- dryRun: z.boolean().optional().describe("Parse and display steps without executing"),
797
- fuzzyMatch: z.boolean().optional().describe("Use case-insensitive fuzzy matching for assertions"),
798
- }, async ({ filepath, dryRun, fuzzyMatch }) => {
799
- const fs = await import("fs");
800
- if (!fs.existsSync(filepath)) {
801
- return { content: [{ type: "text", text: JSON.stringify({ error: `Test file not found: ${filepath}` }) }] };
802
- }
803
- const fileContent = fs.readFileSync(filepath, "utf-8");
804
- const suiteName = filepath.split("/").pop()?.replace(/\.[^.]+$/, "") || "Test Suite";
805
- const suite = parseNLTestSuite(fileContent, suiteName);
806
- if (dryRun) {
807
- const dryResult = dryRunNLTestSuite(suite);
808
- return { content: [{ type: "text", text: JSON.stringify(dryResult, null, 2) }] };
809
- }
810
- const result = await runNLTestSuite(suite, { fuzzyMatch: fuzzyMatch || false });
811
- return {
812
- content: [
813
- {
814
- type: "text",
815
- text: JSON.stringify({
816
- name: result.name,
817
- total: result.summary.total,
818
- passed: result.summary.passed,
819
- failed: result.summary.failed,
820
- passRate: `${result.summary.passRate.toFixed(1)}%`,
821
- // v11.6.0: Step-level statistics for better granularity
822
- totalSteps: result.summary.totalSteps,
823
- passedSteps: result.summary.passedSteps,
824
- failedSteps: result.summary.failedSteps,
825
- stepPassRate: result.summary.stepPassRate ? `${result.summary.stepPassRate.toFixed(1)}%` : undefined,
826
- duration: result.duration,
827
- recommendations: result.recommendations,
828
- testResults: result.testResults.map(t => ({
829
- name: t.name,
830
- passed: t.passed,
831
- duration: t.duration,
832
- error: t.error,
833
- steps: t.stepResults.map(s => ({
834
- instruction: s.instruction,
835
- parsed: s.parsed,
836
- passed: s.passed,
837
- duration: s.duration,
838
- error: s.error,
839
- actualValue: s.actualValue,
840
- })),
841
- })),
842
- }, null, 2),
843
- },
844
- ],
845
- };
846
- });
847
- server.tool("nl_test_inline", "Run natural language tests from inline content. Returns step-level results with enriched error info, partial matches, and suggestions.", {
848
- content: z.string().describe("Test content with instructions like 'go to https://...' and 'click login'"),
849
- name: z.string().optional().describe("Name for the test suite"),
850
- dryRun: z.boolean().optional().describe("Parse and display steps without executing"),
851
- fuzzyMatch: z.boolean().optional().describe("Use case-insensitive fuzzy matching for assertions"),
852
- }, async ({ content, name, dryRun, fuzzyMatch }) => {
853
- const suite = parseNLTestSuite(content, name || "Inline Test");
854
- if (dryRun) {
855
- const dryResult = dryRunNLTestSuite(suite);
856
- return { content: [{ type: "text", text: JSON.stringify(dryResult, null, 2) }] };
857
- }
858
- const result = await runNLTestSuite(suite, { fuzzyMatch: fuzzyMatch || false });
859
- return {
860
- content: [
861
- {
862
- type: "text",
863
- text: JSON.stringify({
864
- name: result.name,
865
- total: result.summary.total,
866
- passed: result.summary.passed,
867
- failed: result.summary.failed,
868
- passRate: `${result.summary.passRate.toFixed(1)}%`,
869
- // v11.6.0: Step-level statistics for better granularity
870
- totalSteps: result.summary.totalSteps,
871
- passedSteps: result.summary.passedSteps,
872
- failedSteps: result.summary.failedSteps,
873
- stepPassRate: result.summary.stepPassRate ? `${result.summary.stepPassRate.toFixed(1)}%` : undefined,
874
- duration: result.duration,
875
- recommendations: result.recommendations,
876
- testResults: result.testResults.map(t => ({
877
- name: t.name,
878
- passed: t.passed,
879
- duration: t.duration,
880
- error: t.error,
881
- steps: t.stepResults.map(s => ({
882
- instruction: s.instruction,
883
- parsed: s.parsed,
884
- passed: s.passed,
885
- duration: s.duration,
886
- error: s.error,
887
- actualValue: s.actualValue,
888
- })),
889
- })),
890
- }, null, 2),
891
- },
892
- ],
893
- };
894
- });
895
- server.tool("repair_test", "AI-powered test repair for broken tests", {
896
- testName: z.string().describe("Name for the test"),
897
- steps: z.array(z.string()).describe("Test step instructions"),
898
- autoApply: z.boolean().optional().describe("Automatically apply repairs"),
899
- }, async ({ testName, steps, autoApply }) => {
900
- const testCase = {
901
- name: testName,
902
- steps: steps.map(instruction => ({
903
- instruction,
904
- action: "unknown",
905
- })),
906
- };
907
- const result = await repairTest(testCase, { autoApply: autoApply || false });
908
- return {
909
- content: [
910
- {
911
- type: "text",
912
- text: JSON.stringify({
913
- originalTest: result.originalTest.name,
914
- failedSteps: result.failedSteps,
915
- repairedSteps: result.repairedSteps,
916
- repairedTestPasses: result.repairedTestPasses,
917
- repairs: result.failureAnalyses.map(a => ({
918
- step: a.step.instruction,
919
- error: a.error,
920
- suggestion: a.suggestions[0]?.suggestedInstruction || "No suggestion",
921
- })),
922
- }, null, 2),
923
- },
924
- ],
925
- };
926
- });
927
- server.tool("detect_flaky_tests", "Detect flaky/unreliable tests by running multiple times", {
928
- testContent: z.string().describe("Test content to analyze"),
929
- runs: z.number().optional().default(5).describe("Number of times to run each test"),
930
- threshold: z.number().optional().default(20).describe("Flakiness threshold percentage"),
931
- }, async ({ testContent, runs, threshold }) => {
932
- const suite = parseNLTestSuite(testContent, "Flaky Test Analysis");
933
- const result = await detectFlakyTests(suite, { runs, flakinessThreshold: threshold });
934
- return {
935
- content: [
936
- {
937
- type: "text",
938
- text: JSON.stringify({
939
- suiteName: result.suiteName,
940
- totalTests: result.summary.totalTests,
941
- stablePass: result.summary.stablePassTests,
942
- stableFail: result.summary.stableFailTests,
943
- flakyTests: result.summary.flakyTests,
944
- overallFlakiness: `${result.summary.overallFlakinessScore.toFixed(1)}%`,
945
- analyses: result.testAnalyses.map(a => ({
946
- test: a.testName,
947
- classification: a.classification,
948
- passRate: `${((a.passCount / a.totalRuns) * 100).toFixed(0)}%`,
949
- flakiness: `${a.flakinessScore}%`,
950
- })),
951
- }, null, 2),
952
- },
953
- ],
954
- };
955
- });
956
- server.tool("coverage_map", "Generate test coverage map for a site", {
957
- baseUrl: z.string().url().describe("Base URL to analyze"),
958
- testFiles: z.array(z.string()).describe("Array of test file paths"),
959
- maxPages: z.number().optional().default(100).describe("Maximum pages to crawl"),
960
- }, async ({ baseUrl, testFiles, maxPages }) => {
961
- const result = await generateCoverageMap(baseUrl, testFiles, { maxPages });
962
- return {
963
- content: [
964
- {
965
- type: "text",
966
- text: JSON.stringify({
967
- totalPages: result.sitePages.length,
968
- testedPages: result.testedPages.length,
969
- untestedPages: result.analysis.untestedPages,
970
- overallCoverage: `${result.analysis.coveragePercent.toFixed(1)}%`,
971
- gaps: result.gaps.slice(0, 10).map(g => ({
972
- url: g.page.url,
973
- priority: g.priority,
974
- reason: g.reason,
975
- })),
976
- }, null, 2),
977
- },
978
- ],
979
- };
980
- });
981
- // =========================================================================
982
- // Analysis Tools (v4.0.0+)
983
- // =========================================================================
984
- server.tool("hunt_bugs", "Autonomous bug hunting - crawl and find issues. Returns bugs with severity, selector, and actionable recommendation for each issue found.", {
985
- url: z.string().url().describe("Starting URL to hunt from"),
986
- maxPages: z.number().optional().default(10).describe("Maximum pages to visit"),
987
- timeout: z.number().optional().default(60000).describe("Timeout in milliseconds"),
988
- }, async ({ url, maxPages, timeout }) => {
989
- const b = await getBrowser();
990
- const result = await huntBugs(b, url, { maxPages, timeout });
991
- return {
992
- content: [
993
- {
994
- type: "text",
995
- text: JSON.stringify({
996
- pagesVisited: result.pagesVisited,
997
- bugsFound: result.bugs.length,
998
- duration: result.duration,
999
- bugs: result.bugs.slice(0, 10).map(bug => ({
1000
- type: bug.type,
1001
- severity: bug.severity,
1002
- description: bug.description,
1003
- url: bug.url,
1004
- selector: bug.selector,
1005
- recommendation: bug.recommendation,
1006
- })),
1007
- }, null, 2),
1008
- },
1009
- ],
1010
- };
1011
- });
1012
- server.tool("chaos_test", "Inject failures and test resilience", {
1013
- url: z.string().url().describe("URL to test"),
1014
- networkLatency: z.number().optional().describe("Simulate network latency (ms)"),
1015
- offline: z.boolean().optional().describe("Simulate offline mode"),
1016
- blockUrls: z.array(z.string()).optional().describe("URL patterns to block"),
1017
- }, async ({ url, networkLatency, offline, blockUrls }) => {
1018
- const b = await getBrowser();
1019
- try {
1020
- const result = await runChaosTest(b, url, { networkLatency, offline, blockUrls });
1021
- return {
1022
- content: [
1023
- {
1024
- type: "text",
1025
- text: JSON.stringify({
1026
- passed: result.passed,
1027
- errors: result.errors,
1028
- duration: result.duration,
1029
- // v16.11.0: Include impact analysis in response
1030
- impact: result.impact,
1031
- }, null, 2),
1032
- },
1033
- ],
1034
- };
1035
- }
1036
- catch (error) {
1037
- // v16.11.0: Graceful error handling for chaos test crashes
1038
- // Attempt browser recovery to prevent server crash
1039
- try {
1040
- await b.recoverBrowser();
1041
- }
1042
- catch {
1043
- // Browser recovery failed, but continue with error response
1044
- }
1045
- return {
1046
- content: [
1047
- {
1048
- type: "text",
1049
- text: JSON.stringify({
1050
- passed: false,
1051
- errors: [`Chaos test crashed: ${error.message}`],
1052
- duration: 0,
1053
- impact: {
1054
- loadTimeMs: 0,
1055
- blockedResources: [],
1056
- failedResources: [],
1057
- delayedResources: [],
1058
- pageCompleted: false,
1059
- pageInteractive: false,
1060
- consoleErrors: 0,
1061
- degradationSummary: ["Test crashed - browser recovered"],
1062
- },
1063
- recovered: true,
1064
- }, null, 2),
1065
- },
1066
- ],
1067
- };
1068
- }
1069
- });
1070
- server.tool("compare_personas", "Compare how different user personas experience a journey. In Claude Code sessions (no API key), use compare_personas_init and compare_personas_complete instead for the bridge workflow.", {
1071
- url: z.string().url().describe("Starting URL"),
1072
- goal: z.string().describe("Goal to accomplish"),
1073
- personas: z.array(z.string()).describe("Persona names to compare"),
1074
- }, async ({ url, goal, personas }) => {
1075
- // v10.10.0: Check if API key is configured (not just env vars)
1076
- // The env var check didn't work on remote servers
1077
- const hasApiKey = isApiKeyConfigured();
1078
- if (!hasApiKey) {
1079
- // Return instructions for Claude Code to use the bridge workflow
1080
- return {
1081
- content: [
1082
- {
1083
- type: "text",
1084
- text: JSON.stringify({
1085
- mode: "bridge",
1086
- message: "Running in Claude Code session - use the bridge workflow for API-free persona comparison",
1087
- instructions: `
1088
- COMPARE PERSONAS BRIDGE WORKFLOW (No API Key Required):
1089
-
1090
- 1. Call compare_personas_init with your URL, goal, and personas list
1091
- 2. For each persona returned, run a cognitive_journey_init and drive the journey using browser tools
1092
- 3. After all journeys complete, call compare_personas_complete with the results
1093
-
1094
- Example:
1095
- 1. compare_personas_init({ url: "${url}", goal: "${goal}", personas: ${JSON.stringify(personas)} })
1096
- 2. For each persona: cognitive_journey_init → navigate/click/fill → track state
1097
- 3. compare_personas_complete({ journeyResults: [...], url: "${url}", goal: "${goal}" })
1098
- `,
1099
- url,
1100
- goal,
1101
- personas,
1102
- }, null, 2),
1103
- },
1104
- ],
1105
- };
1106
- }
1107
- // Standard API-based comparison
1108
- const result = await comparePersonas({
1109
- startUrl: url,
1110
- goal,
1111
- personas,
1112
- });
1113
- return {
1114
- content: [
1115
- {
1116
- type: "text",
1117
- text: JSON.stringify({
1118
- url: result.url,
1119
- goal: result.goal,
1120
- personasCompared: result.personas.length,
1121
- summary: result.summary,
1122
- }, null, 2),
1123
- },
1124
- ],
1125
- };
1126
- });
1127
- server.tool("compare_personas_init", "Initialize persona comparison for Claude Code bridge workflow. Returns persona profiles and instructions for running journeys without API key.", {
1128
- url: z.string().url().describe("Starting URL for all journeys"),
1129
- goal: z.string().describe("Goal to accomplish"),
1130
- personas: z.array(z.string()).describe("Persona names to compare (e.g., ['first-timer', 'power-user', 'elderly-user'])"),
1131
- }, async ({ url, goal, personas }) => {
1132
- // Gather persona profiles
1133
- // v16.14.1: Use getAnyPersona to find personas in ALL registries
1134
- const personaProfiles = personas.map((personaName) => {
1135
- const existingPersona = getAnyPersona(personaName);
1136
- let personaObj;
1137
- if (!existingPersona) {
1138
- // Only create generic stub if persona truly doesn't exist
1139
- personaObj = createCognitivePersona(personaName, personaName, {});
1140
- }
1141
- else {
1142
- personaObj = existingPersona;
1143
- }
1144
- const profile = getCognitiveProfile(personaObj);
1145
- return {
1146
- name: personaName,
1147
- description: personaObj.description,
1148
- demographics: personaObj.demographics,
1149
- cognitiveTraits: profile.traits,
1150
- attentionPattern: profile.attentionPattern,
1151
- decisionStyle: profile.decisionStyle,
1152
- };
1153
- });
1154
- return {
1155
- content: [
1156
- {
1157
- type: "text",
1158
- text: JSON.stringify({
1159
- mode: "compare_personas_bridge",
1160
- url,
1161
- goal,
1162
- personaCount: personas.length,
1163
- personas: personaProfiles,
1164
- instructions: `
1165
- PERSONA COMPARISON BRIDGE WORKFLOW:
1166
-
1167
- You have ${personas.length} personas to compare. For each persona:
1168
-
1169
- 1. Call cognitive_journey_init with the persona name, goal, and URL
1170
- 2. Drive the journey using browser tools (navigate, click, fill, screenshot)
1171
- 3. Track cognitive state using cognitive_journey_update_state
1172
- 4. Continue until goal achieved or persona abandons
1173
- 5. Record the final result
1174
-
1175
- After ALL personas complete their journeys, call compare_personas_complete with:
1176
- {
1177
- url: "${url}",
1178
- goal: "${goal}",
1179
- journeyResults: [
1180
- {
1181
- persona: "persona-name",
1182
- goalAchieved: true/false,
1183
- totalTime: seconds,
1184
- stepCount: number,
1185
- finalState: { patienceRemaining, frustrationLevel, confusionLevel },
1186
- abandonmentReason: null or "patience" | "frustration" | "confusion" | "timeout" | "loop",
1187
- frictionPoints: ["description of friction point", ...]
1188
- },
1189
- // ... one for each persona
1190
- ]
1191
- }
1192
-
1193
- PERSONA ORDER:
1194
- ${personaProfiles.map((p, i) => `${i + 1}. ${p.name} - ${p.description}`).join("\n")}
1195
-
1196
- Begin with the first persona: ${personas[0]}
1197
- `,
1198
- }, null, 2),
1199
- },
1200
- ],
1201
- };
1202
- });
1203
- server.tool("compare_personas_complete", "Complete persona comparison by aggregating journey results. Call this after running all persona journeys via the bridge workflow.", {
1204
- url: z.string().url().describe("The URL that was tested"),
1205
- goal: z.string().describe("The goal that was attempted"),
1206
- journeyResults: z.array(z.object({
1207
- persona: z.string().describe("Persona name"),
1208
- goalAchieved: z.boolean().describe("Whether the goal was achieved"),
1209
- totalTime: z.number().describe("Total time in seconds"),
1210
- stepCount: z.number().describe("Number of steps taken"),
1211
- finalState: z.object({
1212
- patienceRemaining: z.number(),
1213
- frustrationLevel: z.number(),
1214
- confusionLevel: z.number(),
1215
- }).describe("Final cognitive state"),
1216
- abandonmentReason: z.enum(["patience", "frustration", "confusion", "timeout", "loop"]).nullable().describe("Why journey ended if not goal achieved"),
1217
- frictionPoints: z.array(z.string()).describe("List of friction point descriptions"),
1218
- })).describe("Results from each persona journey"),
1219
- }, async ({ url, goal, journeyResults }) => {
1220
- const startTime = Date.now();
1221
- // Calculate rankings and generate comparison
1222
- const successfulResults = journeyResults.filter((r) => r.goalAchieved);
1223
- const failedResults = journeyResults.filter((r) => !r.goalAchieved);
1224
- const sortedByTime = [...successfulResults].sort((a, b) => a.totalTime - b.totalTime);
1225
- const sortedByFriction = [...journeyResults].sort((a, b) => b.frictionPoints.length - a.frictionPoints.length);
1226
- // Collect all friction points
1227
- const allFrictionPoints = journeyResults.flatMap((r) => r.frictionPoints);
1228
- const frictionCounts = allFrictionPoints.reduce((acc, fp) => {
1229
- acc[fp] = (acc[fp] || 0) + 1;
1230
- return acc;
1231
- }, {});
1232
- const commonFriction = Object.entries(frictionCounts)
1233
- .filter(([_, count]) => count > 1)
1234
- .sort((a, b) => b[1] - a[1])
1235
- .slice(0, 5)
1236
- .map(([fp]) => fp);
1237
- // Generate recommendations
1238
- const recommendations = [];
1239
- // Abandonment analysis
1240
- const abandonedByPatience = failedResults.filter((r) => r.abandonmentReason === "patience");
1241
- const abandonedByFrustration = failedResults.filter((r) => r.abandonmentReason === "frustration");
1242
- const abandonedByConfusion = failedResults.filter((r) => r.abandonmentReason === "confusion");
1243
- if (abandonedByPatience.length > 0) {
1244
- recommendations.push(`${abandonedByPatience.length} persona(s) abandoned due to PATIENCE exhaustion: ${abandonedByPatience.map((r) => r.persona).join(", ")} - consider shorter flows`);
1245
- }
1246
- if (abandonedByFrustration.length > 0) {
1247
- recommendations.push(`${abandonedByFrustration.length} persona(s) abandoned due to FRUSTRATION: ${abandonedByFrustration.map((r) => r.persona).join(", ")} - review error messages and feedback`);
1248
- }
1249
- if (abandonedByConfusion.length > 0) {
1250
- recommendations.push(`${abandonedByConfusion.length} persona(s) abandoned due to CONFUSION: ${abandonedByConfusion.map((r) => r.persona).join(", ")} - improve UI clarity and labeling`);
1251
- }
1252
- // Friction analysis
1253
- if (sortedByFriction[0]?.frictionPoints.length > 0) {
1254
- const worstPersona = sortedByFriction[0];
1255
- const avgFrustration = worstPersona.finalState.frustrationLevel;
1256
- recommendations.push(`"${worstPersona.persona}" experienced the most friction (${worstPersona.frictionPoints.length} points, ${Math.round(avgFrustration * 100)}% frustration)`);
1257
- }
1258
- // Common friction points
1259
- if (commonFriction.length > 0) {
1260
- recommendations.push(`Common friction across personas: ${commonFriction.slice(0, 2).join("; ")}`);
1261
- }
1262
- if (recommendations.length === 0) {
1263
- recommendations.push("All personas completed the journey without significant cognitive barriers");
1264
- }
1265
- const avgTime = successfulResults.length > 0
1266
- ? successfulResults.reduce((sum, r) => sum + r.totalTime, 0) / successfulResults.length
1267
- : 0;
1268
- const comparison = {
1269
- url,
1270
- goal,
1271
- timestamp: new Date().toISOString(),
1272
- duration: Date.now() - startTime,
1273
- personas: journeyResults.map((r) => ({
1274
- persona: r.persona,
1275
- success: r.goalAchieved,
1276
- totalTime: r.totalTime * 1000, // Convert to ms for consistency
1277
- stepCount: r.stepCount,
1278
- frictionCount: r.frictionPoints.length,
1279
- frictionPoints: r.frictionPoints,
1280
- cognitive: {
1281
- patienceRemaining: r.finalState.patienceRemaining,
1282
- frustrationLevel: r.finalState.frustrationLevel,
1283
- confusionLevel: r.finalState.confusionLevel,
1284
- abandonmentReason: r.abandonmentReason,
1285
- },
1286
- })),
1287
- summary: {
1288
- totalPersonas: journeyResults.length,
1289
- successCount: successfulResults.length,
1290
- failureCount: failedResults.length,
1291
- fastestPersona: sortedByTime[0]?.persona || "N/A",
1292
- slowestPersona: sortedByTime[sortedByTime.length - 1]?.persona || "N/A",
1293
- mostFriction: sortedByFriction[0]?.persona || "N/A",
1294
- leastFriction: sortedByFriction[sortedByFriction.length - 1]?.persona || "N/A",
1295
- avgCompletionTime: Math.round(avgTime * 1000),
1296
- commonFrictionPoints: commonFriction,
1297
- },
1298
- recommendations,
1299
- };
1300
- return {
1301
- content: [
1302
- {
1303
- type: "text",
1304
- text: JSON.stringify(comparison, null, 2),
1305
- },
1306
- ],
1307
- };
1308
- });
1309
- server.tool("find_element_by_intent", "AI-powered semantic element finding with ARIA-first selector strategy. Prioritizes aria-label > role > semantic HTML > ID > name > class. Returns selectorType, accessibilityScore (0-1), and alternatives. Use verbose=true for enriched failure responses.", {
1310
- intent: z.string().describe("Natural language description like 'the cheapest product' or 'login form'"),
1311
- verbose: z.boolean().optional().describe("Include alternative matches with confidence scores and AI suggestions"),
1312
- }, async ({ intent, verbose }) => {
1313
- const b = await getBrowser();
1314
- const result = await findElementByIntent(b, intent, { verbose });
1315
- if (result && result.confidence > 0) {
1316
- return {
1317
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1318
- };
1319
- }
1320
- return {
1321
- content: [{ type: "text", text: JSON.stringify(result || { found: false, message: "No matching element found" }, null, 2) }],
1322
- };
1323
- });
1324
- // =========================================================================
1325
- // Cognitive Simulation Tools (v8.3.0)
1326
- // =========================================================================
1327
- server.tool("cognitive_journey_init", "Initialize a cognitive user journey simulation. Returns the persona's cognitive profile, initial state, and abandonment thresholds. The actual simulation is driven by the LLM using browser tools (navigate, click, fill, screenshot) while tracking cognitive state.", {
1328
- persona: z.string().describe("Persona name (e.g., 'first-timer', 'elderly-user', 'power-user') or custom description"),
1329
- goal: z.string().describe("What the simulated user is trying to accomplish"),
1330
- startUrl: z.string().url().describe("Starting URL for the journey"),
1331
- customTraits: z.object({
1332
- // Core 7 traits
1333
- patience: z.number().min(0).max(1).optional(),
1334
- riskTolerance: z.number().min(0).max(1).optional(),
1335
- comprehension: z.number().min(0).max(1).optional(),
1336
- persistence: z.number().min(0).max(1).optional(),
1337
- curiosity: z.number().min(0).max(1).optional(),
1338
- workingMemory: z.number().min(0).max(1).optional(),
1339
- readingTendency: z.number().min(0).max(1).optional(),
1340
- // v16.11.0: Extended traits (18 more = 25 total)
1341
- resilience: z.number().min(0).max(1).optional(),
1342
- selfEfficacy: z.number().min(0).max(1).optional(),
1343
- satisficing: z.number().min(0).max(1).optional(),
1344
- trustCalibration: z.number().min(0).max(1).optional(),
1345
- interruptRecovery: z.number().min(0).max(1).optional(),
1346
- informationForaging: z.number().min(0).max(1).optional(),
1347
- changeBlindness: z.number().min(0).max(1).optional(),
1348
- anchoringBias: z.number().min(0).max(1).optional(),
1349
- timeHorizon: z.number().min(0).max(1).optional(),
1350
- attributionStyle: z.number().min(0).max(1).optional(),
1351
- metacognitivePlanning: z.number().min(0).max(1).optional(),
1352
- proceduralFluency: z.number().min(0).max(1).optional(),
1353
- transferLearning: z.number().min(0).max(1).optional(),
1354
- authoritySensitivity: z.number().min(0).max(1).optional(),
1355
- emotionalContagion: z.number().min(0).max(1).optional(),
1356
- fearOfMissingOut: z.number().min(0).max(1).optional(),
1357
- socialProofSensitivity: z.number().min(0).max(1).optional(),
1358
- mentalModelRigidity: z.number().min(0).max(1).optional(),
1359
- }).optional().describe("Override specific cognitive traits (25 available)"),
1360
- }, async ({ persona: personaName, goal, startUrl, customTraits }) => {
1361
- // Get or create persona
1362
- // v16.14.1: Use getAnyPersona to find personas in ALL registries
1363
- const existingPersona = getAnyPersona(personaName);
1364
- let personaObj;
1365
- if (!existingPersona) {
1366
- // Create from description
1367
- personaObj = createCognitivePersona(personaName, personaName, customTraits || {});
1368
- }
1369
- else if (customTraits) {
1370
- // v16.11.0: Full 25-trait default set (was only 7, causing trait dropout)
1371
- const defaultTraits = {
1372
- // Core 7 traits
1373
- patience: 0.5,
1374
- riskTolerance: 0.5,
1375
- comprehension: 0.5,
1376
- persistence: 0.5,
1377
- curiosity: 0.5,
1378
- workingMemory: 0.5,
1379
- readingTendency: 0.5,
1380
- // Tier 1: Core (5 more)
1381
- resilience: 0.5,
1382
- selfEfficacy: 0.5,
1383
- satisficing: 0.5,
1384
- trustCalibration: 0.5,
1385
- interruptRecovery: 0.5,
1386
- // Tier 2-6: Extended (13 more)
1387
- informationForaging: 0.5,
1388
- changeBlindness: 0.3,
1389
- anchoringBias: 0.5,
1390
- timeHorizon: 0.5,
1391
- attributionStyle: 0.5,
1392
- metacognitivePlanning: 0.5,
1393
- proceduralFluency: 0.5,
1394
- transferLearning: 0.5,
1395
- authoritySensitivity: 0.5,
1396
- emotionalContagion: 0.5,
1397
- fearOfMissingOut: 0.5,
1398
- socialProofSensitivity: 0.5,
1399
- mentalModelRigidity: 0.5,
1400
- };
1401
- personaObj = {
1402
- ...existingPersona,
1403
- cognitiveTraits: {
1404
- ...defaultTraits,
1405
- ...(existingPersona.cognitiveTraits || {}),
1406
- ...customTraits,
1407
- },
1408
- };
1409
- }
1410
- else {
1411
- personaObj = existingPersona;
1412
- }
1413
- // Get cognitive profile
1414
- const profile = getCognitiveProfile(personaObj);
1415
- // Initial cognitive state
1416
- const initialState = {
1417
- patienceRemaining: 1.0,
1418
- confusionLevel: 0.0,
1419
- frustrationLevel: 0.0,
1420
- goalProgress: 0.0,
1421
- confidenceLevel: 0.5,
1422
- currentMood: "neutral",
1423
- memory: {
1424
- pagesVisited: [startUrl],
1425
- actionsAttempted: [],
1426
- errorsEncountered: [],
1427
- backtrackCount: 0,
1428
- },
1429
- timeElapsed: 0,
1430
- stepCount: 0,
1431
- };
1432
- // Abandonment thresholds (adjusted by persona traits)
1433
- const traits = profile.traits;
1434
- const thresholds = {
1435
- patienceMin: 0.1,
1436
- confusionMax: traits.comprehension < 0.4 ? 0.6 : 0.8,
1437
- frustrationMax: traits.patience < 0.3 ? 0.7 : 0.85,
1438
- maxStepsWithoutProgress: traits.persistence > 0.7 ? 15 : 10,
1439
- loopDetectionThreshold: 3,
1440
- timeLimit: traits.patience > 0.7 ? 180 : (traits.patience < 0.3 ? 60 : 120),
1441
- };
1442
- // Navigate to start URL
1443
- const b = await getBrowser();
1444
- await b.navigate(startUrl);
1445
- // v16.12.0: Include persona values for influence pattern analysis
1446
- const personaValues = getPersonaValues(personaObj.name);
1447
- const influencePatterns = personaValues
1448
- ? rankInfluencePatternsForProfile(personaValues).slice(0, 5) // Top 5 most effective patterns
1449
- : undefined;
1450
- return {
1451
- content: [
1452
- {
1453
- type: "text",
1454
- text: JSON.stringify({
1455
- persona: {
1456
- name: personaObj.name,
1457
- description: personaObj.description,
1458
- demographics: personaObj.demographics,
1459
- values: personaValues ? {
1460
- schwartz: {
1461
- selfDirection: personaValues.selfDirection,
1462
- stimulation: personaValues.stimulation,
1463
- hedonism: personaValues.hedonism,
1464
- achievement: personaValues.achievement,
1465
- power: personaValues.power,
1466
- security: personaValues.security,
1467
- conformity: personaValues.conformity,
1468
- tradition: personaValues.tradition,
1469
- benevolence: personaValues.benevolence,
1470
- universalism: personaValues.universalism,
1471
- },
1472
- higherOrder: {
1473
- openness: personaValues.openness,
1474
- selfEnhancement: personaValues.selfEnhancement,
1475
- conservation: personaValues.conservation,
1476
- selfTranscendence: personaValues.selfTranscendence,
1477
- },
1478
- sdt: {
1479
- autonomyNeed: personaValues.autonomyNeed,
1480
- competenceNeed: personaValues.competenceNeed,
1481
- relatednessNeed: personaValues.relatednessNeed,
1482
- },
1483
- maslowLevel: personaValues.maslowLevel,
1484
- } : undefined,
1485
- influenceSusceptibility: influencePatterns?.map(ip => ({
1486
- pattern: ip.pattern.name,
1487
- susceptibility: ip.susceptibility,
1488
- })),
1489
- },
1490
- cognitiveProfile: profile,
1491
- initialState,
1492
- abandonmentThresholds: thresholds,
1493
- goal,
1494
- startUrl,
1495
- instructions: `
1496
- COGNITIVE JOURNEY SIMULATION INSTRUCTIONS:
1497
-
1498
- You are now simulating a "${personaObj.name}" user with these cognitive traits:
1499
- - Patience: ${profile.traits.patience.toFixed(2)} ${profile.traits.patience < 0.3 ? "(impatient - will give up quickly)" : profile.traits.patience > 0.7 ? "(patient - will persist)" : "(moderate)"}
1500
- - Risk Tolerance: ${profile.traits.riskTolerance.toFixed(2)} ${profile.traits.riskTolerance < 0.3 ? "(cautious - hesitates)" : profile.traits.riskTolerance > 0.7 ? "(bold - clicks freely)" : "(moderate)"}
1501
- - Comprehension: ${profile.traits.comprehension.toFixed(2)} ${profile.traits.comprehension < 0.3 ? "(struggles with UI)" : profile.traits.comprehension > 0.7 ? "(expert at UI patterns)" : "(moderate)"}
1502
- - Reading Tendency: ${profile.traits.readingTendency.toFixed(2)} ${profile.traits.readingTendency < 0.3 ? "(scans only)" : profile.traits.readingTendency > 0.7 ? "(reads everything)" : "(selective reader)"}
1503
-
1504
- Attention Pattern: ${profile.attentionPattern}
1505
- Decision Style: ${profile.decisionStyle}
1506
-
1507
- GOAL: "${goal}"
1508
-
1509
- SIMULATION LOOP:
1510
- 1. PERCEIVE - Use screenshot/snapshot to see the page. Filter by attention pattern.
1511
- 2. COMPREHEND - Interpret elements as this persona would (lower comprehension = more confusion)
1512
- 3. DECIDE - Choose action based on traits. Generate inner monologue.
1513
- 4. EXECUTE - Use click/fill/navigate tools.
1514
- 5. EVALUATE - Update cognitive state after each action:
1515
- - patienceRemaining -= 0.02 + (frustrationLevel × 0.05)
1516
- - confusionLevel changes based on UI clarity
1517
- - frustrationLevel increases on failures
1518
- 6. CHECK ABANDONMENT - If thresholds exceeded, end journey with appropriate message.
1519
- 7. LOOP - Return to PERCEIVE until goal achieved or abandoned.
1520
-
1521
- ABANDONMENT TRIGGERS:
1522
- - Patience < ${thresholds.patienceMin}: "This is taking too long. I give up."
1523
- - Confusion > ${thresholds.confusionMax} for 30s: "I have no idea what to do."
1524
- - Frustration > ${thresholds.frustrationMax}: "This is so frustrating!"
1525
- - No progress after ${thresholds.maxStepsWithoutProgress} steps: "I'm not getting anywhere."
1526
- - Same page ${thresholds.loopDetectionThreshold}x: "I keep ending up here."
1527
- - Time > ${thresholds.timeLimit}s: "I've spent too long on this."
1528
-
1529
- Begin the simulation now. Narrate your thoughts as this persona.
1530
- `,
1531
- }, null, 2),
1532
- },
1533
- ],
1534
- };
1535
- });
1536
- server.tool("cognitive_journey_update_state", "Update the cognitive state during a journey simulation. Call this after each action to track mental state.", {
1537
- currentState: z.object({
1538
- patienceRemaining: z.number(),
1539
- confusionLevel: z.number(),
1540
- frustrationLevel: z.number(),
1541
- goalProgress: z.number(),
1542
- confidenceLevel: z.number(),
1543
- currentMood: z.enum(["neutral", "hopeful", "confused", "frustrated", "defeated", "relieved"]),
1544
- stepCount: z.number(),
1545
- timeElapsed: z.number(),
1546
- }).describe("Current cognitive state"),
1547
- actionResult: z.object({
1548
- success: z.boolean(),
1549
- wasConfusing: z.boolean().optional(),
1550
- progressMade: z.boolean().optional(),
1551
- wentBack: z.boolean().optional(),
1552
- }).describe("Result of the last action"),
1553
- personaTraits: z.object({
1554
- patience: z.number(),
1555
- riskTolerance: z.number(),
1556
- comprehension: z.number(),
1557
- persistence: z.number(),
1558
- }).describe("Persona traits affecting state changes"),
1559
- }, async ({ currentState, actionResult, personaTraits }) => {
1560
- // Calculate new state based on action result
1561
- let newPatienceRemaining = currentState.patienceRemaining - 0.02;
1562
- let newConfusionLevel = currentState.confusionLevel;
1563
- let newFrustrationLevel = currentState.frustrationLevel;
1564
- let newConfidenceLevel = currentState.confidenceLevel;
1565
- let newMood = currentState.currentMood;
1566
- // Apply frustration decay on patience
1567
- newPatienceRemaining -= currentState.frustrationLevel * 0.05;
1568
- if (actionResult.success) {
1569
- // Success reduces confusion and frustration
1570
- newConfusionLevel = Math.max(0, newConfusionLevel - 0.1);
1571
- newFrustrationLevel = Math.max(0, newFrustrationLevel - 0.05);
1572
- if (actionResult.progressMade) {
1573
- newConfidenceLevel = Math.min(1, newConfidenceLevel + 0.1);
1574
- if (newMood === "confused" || newMood === "frustrated") {
1575
- newMood = "hopeful";
1576
- }
1577
- }
1578
- }
1579
- else {
1580
- // Failure increases frustration
1581
- newFrustrationLevel = Math.min(1, newFrustrationLevel + 0.2);
1582
- if (newFrustrationLevel > 0.7) {
1583
- newMood = "frustrated";
1584
- }
1585
- if (newFrustrationLevel > 0.8 && personaTraits.persistence < 0.5) {
1586
- newMood = "defeated";
1587
- }
1588
- }
1589
- if (actionResult.wasConfusing) {
1590
- // Confusion builds based on comprehension
1591
- newConfusionLevel = Math.min(1, newConfusionLevel + (1 - personaTraits.comprehension) * 0.15);
1592
- if (newConfusionLevel > 0.5 && newMood !== "frustrated") {
1593
- newMood = "confused";
1594
- }
1595
- }
1596
- if (actionResult.wentBack) {
1597
- newConfidenceLevel = Math.max(0, newConfidenceLevel - 0.15);
1598
- }
1599
- const newState = {
1600
- patienceRemaining: Math.max(0, newPatienceRemaining),
1601
- confusionLevel: newConfusionLevel,
1602
- frustrationLevel: newFrustrationLevel,
1603
- confidenceLevel: newConfidenceLevel,
1604
- currentMood: newMood,
1605
- stepCount: currentState.stepCount + 1,
1606
- timeElapsed: currentState.timeElapsed + 2,
1607
- };
1608
- // Check abandonment conditions
1609
- let shouldAbandon = false;
1610
- let abandonmentReason;
1611
- let abandonmentMessage;
1612
- if (newState.patienceRemaining < 0.1) {
1613
- shouldAbandon = true;
1614
- abandonmentReason = "patience";
1615
- abandonmentMessage = "This is taking too long. I give up.";
1616
- }
1617
- else if (newState.frustrationLevel > 0.85) {
1618
- shouldAbandon = true;
1619
- abandonmentReason = "frustration";
1620
- abandonmentMessage = "This is so frustrating! I'm done.";
1621
- }
1622
- else if (newState.confusionLevel > 0.8 && currentState.confusionLevel > 0.8) {
1623
- shouldAbandon = true;
1624
- abandonmentReason = "confusion";
1625
- abandonmentMessage = "I have no idea what I'm supposed to do here.";
1626
- }
1627
- return {
1628
- content: [
1629
- {
1630
- type: "text",
1631
- text: JSON.stringify({
1632
- newState,
1633
- shouldAbandon,
1634
- abandonmentReason,
1635
- abandonmentMessage,
1636
- stateChange: {
1637
- patienceDelta: newState.patienceRemaining - currentState.patienceRemaining,
1638
- confusionDelta: newState.confusionLevel - currentState.confusionLevel,
1639
- frustrationDelta: newState.frustrationLevel - currentState.frustrationLevel,
1640
- },
1641
- }, null, 2),
1642
- },
1643
- ],
1644
- };
1645
- });
1646
- server.tool("list_cognitive_personas", "List all available personas with their cognitive traits (includes accessibility and emotional personas)", {}, async () => {
1647
- // v16.11.0: Include all persona types - BUILTIN + ACCESSIBILITY + EMOTIONAL
1648
- const builtinNames = listPersonas();
1649
- const accessibilityNames = listAccessibilityPersonas();
1650
- // Built-in personas (power-user, first-timer, etc.)
1651
- const builtinPersonas = builtinNames.map(name => {
1652
- const p = getPersona(name);
1653
- if (!p)
1654
- return null;
1655
- const profile = getCognitiveProfile(p);
1656
- // v16.12.0: Include Schwartz values for each persona
1657
- const values = getPersonaValues(p.name);
1658
- return {
1659
- name: p.name,
1660
- description: p.description,
1661
- category: "builtin",
1662
- demographics: p.demographics,
1663
- cognitiveTraits: profile.traits,
1664
- attentionPattern: profile.attentionPattern,
1665
- decisionStyle: profile.decisionStyle,
1666
- values: values ? {
1667
- schwartz: {
1668
- selfDirection: values.selfDirection,
1669
- stimulation: values.stimulation,
1670
- hedonism: values.hedonism,
1671
- achievement: values.achievement,
1672
- power: values.power,
1673
- security: values.security,
1674
- conformity: values.conformity,
1675
- tradition: values.tradition,
1676
- benevolence: values.benevolence,
1677
- universalism: values.universalism,
1678
- },
1679
- higherOrder: {
1680
- openness: values.openness,
1681
- selfEnhancement: values.selfEnhancement,
1682
- conservation: values.conservation,
1683
- selfTranscendence: values.selfTranscendence,
1684
- },
1685
- sdt: {
1686
- autonomyNeed: values.autonomyNeed,
1687
- competenceNeed: values.competenceNeed,
1688
- relatednessNeed: values.relatednessNeed,
1689
- },
1690
- maslowLevel: values.maslowLevel,
1691
- } : undefined,
1692
- };
1693
- }).filter(Boolean);
1694
- // Accessibility personas (motor-tremor, low-vision, adhd, etc.)
1695
- const accessibilityPersonas = accessibilityNames.map(name => {
1696
- const p = getAccessibilityPersona(name);
1697
- if (!p)
1698
- return null;
1699
- // v16.11.0: Compute disabilityType and barrierTypes from accessibilityTraits
1700
- const traits = p.accessibilityTraits;
1701
- let disabilityType = "General accessibility";
1702
- const barrierTypes = [];
1703
- if (traits?.tremor) {
1704
- disabilityType = "Motor impairment (tremor)";
1705
- barrierTypes.push("motor_precision", "touch_target");
1706
- }
1707
- if (traits?.visionLevel !== undefined && traits.visionLevel < 0.5) {
1708
- disabilityType = "Low vision";
1709
- barrierTypes.push("visual_clarity", "contrast");
1710
- }
1711
- if (traits?.colorBlindness) {
1712
- disabilityType = `Color blindness (${traits.colorBlindness})`;
1713
- barrierTypes.push("sensory");
1714
- }
1715
- if (traits?.processingSpeed !== undefined && traits.processingSpeed < 0.6) {
1716
- disabilityType = "Cognitive (Processing)";
1717
- barrierTypes.push("cognitive_load", "temporal");
1718
- }
1719
- if (traits?.attentionSpan !== undefined && traits.attentionSpan < 0.5) {
1720
- if (!disabilityType.includes("Cognitive")) {
1721
- disabilityType = "Cognitive (ADHD/Attention)";
1722
- }
1723
- barrierTypes.push("cognitive_load");
1724
- }
1725
- // Name-based fallback
1726
- if (disabilityType === "General accessibility") {
1727
- if (p.name.includes("deaf") || p.name.includes("hearing"))
1728
- disabilityType = "Hearing impairment";
1729
- else if (p.name.includes("motor"))
1730
- disabilityType = "Motor impairment";
1731
- else if (p.name.includes("vision") || p.name.includes("blind"))
1732
- disabilityType = "Vision impairment";
1733
- else if (p.name.includes("cognitive") || p.name.includes("adhd"))
1734
- disabilityType = "Cognitive";
1735
- }
1736
- // v16.12.0: Include Schwartz values for accessibility personas
1737
- const values = getPersonaValues(p.name);
1738
- return {
1739
- name: p.name,
1740
- description: p.description,
1741
- category: "accessibility",
1742
- disabilityType,
1743
- demographics: p.demographics,
1744
- cognitiveTraits: p.cognitiveTraits || {},
1745
- barrierTypes: [...new Set(barrierTypes)], // Deduplicate
1746
- values: values ? {
1747
- schwartz: {
1748
- selfDirection: values.selfDirection,
1749
- stimulation: values.stimulation,
1750
- hedonism: values.hedonism,
1751
- achievement: values.achievement,
1752
- power: values.power,
1753
- security: values.security,
1754
- conformity: values.conformity,
1755
- tradition: values.tradition,
1756
- benevolence: values.benevolence,
1757
- universalism: values.universalism,
1758
- },
1759
- higherOrder: {
1760
- openness: values.openness,
1761
- selfEnhancement: values.selfEnhancement,
1762
- conservation: values.conservation,
1763
- selfTranscendence: values.selfTranscendence,
1764
- },
1765
- sdt: {
1766
- autonomyNeed: values.autonomyNeed,
1767
- competenceNeed: values.competenceNeed,
1768
- relatednessNeed: values.relatednessNeed,
1769
- },
1770
- maslowLevel: values.maslowLevel,
1771
- } : undefined,
1772
- };
1773
- }).filter(Boolean);
1774
- const allPersonas = [...builtinPersonas, ...accessibilityPersonas];
1775
- return {
1776
- content: [
1777
- {
1778
- type: "text",
1779
- text: JSON.stringify({
1780
- personas: allPersonas,
1781
- count: allPersonas.length,
1782
- categories: {
1783
- builtin: builtinPersonas.length,
1784
- accessibility: accessibilityPersonas.length,
1785
- },
1786
- }, null, 2),
1787
- },
1788
- ],
1789
- };
1790
- });
1791
- // =========================================================================
1792
- // Values System Tools (v16.12.0)
1793
- // Schwartz's 10 Universal Values, Self-Determination Theory, Maslow
1794
- // =========================================================================
1795
- server.tool("persona_values_lookup", "Look up the values profile for a persona (Schwartz's 10 Universal Values, SDT needs, Maslow level). Values describe WHO the persona is at a deeper motivational level, informing influence susceptibility.", {
1796
- persona: z.string().describe("Persona name (e.g., 'first-timer', 'power-user', 'anxious-user')"),
1797
- includeInfluencePatterns: z.boolean().optional().default(true).describe("Include ranked influence patterns this persona is susceptible to"),
1798
- }, async ({ persona, includeInfluencePatterns }) => {
1799
- const values = getPersonaValues(persona);
1800
- if (!values) {
1801
- const availablePersonas = PERSONA_VALUE_PROFILES.map(p => p.personaName);
1802
- return {
1803
- content: [
1804
- {
1805
- type: "text",
1806
- text: JSON.stringify({
1807
- error: `No values profile found for persona: ${persona}`,
1808
- availablePersonas,
1809
- note: "Values are defined for all built-in personas. Custom personas can have values added via the questionnaire.",
1810
- }, null, 2),
1811
- },
1812
- ],
1813
- };
1814
- }
1815
- const profile = PERSONA_VALUE_PROFILES.find(p => p.personaName.toLowerCase() === persona.toLowerCase());
1816
- let influencePatterns;
1817
- if (includeInfluencePatterns) {
1818
- const ranked = rankInfluencePatternsForProfile(values);
1819
- influencePatterns = ranked.slice(0, 7).map(r => ({
1820
- pattern: r.pattern.name,
1821
- susceptibility: r.susceptibility,
1822
- description: r.pattern.description,
1823
- }));
1824
- }
1825
- return {
1826
- content: [
1827
- {
1828
- type: "text",
1829
- text: JSON.stringify({
1830
- persona,
1831
- rationale: profile?.rationale,
1832
- schwartzValues: {
1833
- selfDirection: { value: values.selfDirection, meaning: "Independent thought, creativity, freedom" },
1834
- stimulation: { value: values.stimulation, meaning: "Excitement, novelty, challenge" },
1835
- hedonism: { value: values.hedonism, meaning: "Pleasure, sensuous gratification" },
1836
- achievement: { value: values.achievement, meaning: "Personal success through competence" },
1837
- power: { value: values.power, meaning: "Social status, prestige, control" },
1838
- security: { value: values.security, meaning: "Safety, harmony, stability" },
1839
- conformity: { value: values.conformity, meaning: "Restraint of actions that harm others" },
1840
- tradition: { value: values.tradition, meaning: "Respect for customs, heritage" },
1841
- benevolence: { value: values.benevolence, meaning: "Welfare of close others" },
1842
- universalism: { value: values.universalism, meaning: "Tolerance, social justice, environment" },
1843
- },
1844
- higherOrderValues: {
1845
- openness: { value: values.openness, meaning: "(selfDirection + stimulation) / 2" },
1846
- selfEnhancement: { value: values.selfEnhancement, meaning: "(achievement + power) / 2" },
1847
- conservation: { value: values.conservation, meaning: "(security + conformity + tradition) / 3" },
1848
- selfTranscendence: { value: values.selfTranscendence, meaning: "(benevolence + universalism) / 2" },
1849
- },
1850
- selfDeterminationTheory: {
1851
- autonomyNeed: { value: values.autonomyNeed, meaning: "Need for choice and control" },
1852
- competenceNeed: { value: values.competenceNeed, meaning: "Need to feel capable" },
1853
- relatednessNeed: { value: values.relatednessNeed, meaning: "Need for connection" },
1854
- },
1855
- maslowLevel: {
1856
- level: values.maslowLevel,
1857
- meaning: values.maslowLevel === "physiological" ? "Basic survival needs"
1858
- : values.maslowLevel === "safety" ? "Security and stability"
1859
- : values.maslowLevel === "belonging" ? "Social connection and love"
1860
- : values.maslowLevel === "esteem" ? "Achievement and recognition"
1861
- : "Self-fulfillment and growth",
1862
- },
1863
- influencePatterns,
1864
- researchBasis: {
1865
- schwartz: "Schwartz, S. H. (1992, 2012). Theory of Basic Human Values. DOI: 10.1016/S0065-2601(08)60281-6",
1866
- sdt: "Deci, E. L., & Ryan, R. M. (1985, 2000). Self-Determination Theory. DOI: 10.1037/0003-066X.55.1.68",
1867
- maslow: "Maslow, A. H. (1943). A Theory of Human Motivation. DOI: 10.1037/h0054346",
1868
- },
1869
- }, null, 2),
1870
- },
1871
- ],
1872
- };
1873
- });
1874
- server.tool("list_influence_patterns", "List all research-backed influence/persuasion patterns and which persona values make someone susceptible to each pattern. Based on Cialdini, Kahneman, and behavioral economics research.", {}, async () => {
1875
- // INFLUENCE_PATTERNS is an array of InfluencePattern objects
1876
- const patterns = INFLUENCE_PATTERNS.map(pattern => ({
1877
- name: pattern.name,
1878
- description: pattern.description,
1879
- researchBasis: pattern.researchBasis,
1880
- targetValues: pattern.targetValues,
1881
- mechanism: pattern.mechanism,
1882
- examples: pattern.examples,
1883
- }));
1884
- return {
1885
- content: [
1886
- {
1887
- type: "text",
1888
- text: JSON.stringify({
1889
- count: patterns.length,
1890
- patterns,
1891
- usage: "Use persona_values_lookup to see which patterns a specific persona is susceptible to",
1892
- note: "These patterns describe psychological influence mechanisms. Use ethically for UX optimization, not manipulation.",
1893
- }, null, 2),
1894
- },
1895
- ],
1896
- };
1897
- });
1898
- // =========================================================================
1899
- // Persona Questionnaire Tools (v16.5.0)
1900
- // Research-based persona generation via questionnaire
1901
- // =========================================================================
1902
- server.tool("persona_questionnaire_get", "Get the persona questionnaire for building a custom persona. Returns research-backed questions that map to cognitive traits. Use comprehensive=true for all 25 traits, or leave false for 8 core traits. v16.12.0: Now includes optional category question for disability-specific value safeguards.", {
1903
- comprehensive: z.boolean().optional().default(false).describe("Include all 25 traits (true) or just 8 core traits (false)"),
1904
- traits: z.array(z.string()).optional().describe("Specific trait names to include (overrides comprehensive)"),
1905
- includeCategory: z.boolean().optional().default(true).describe("Include category question for disability-aware values (v16.12.0)"),
1906
- }, async ({ comprehensive, traits, includeCategory }) => {
1907
- const { generatePersonaQuestionnaire, formatForAskUserQuestion, CATEGORY_QUESTION } = await import("./persona-questionnaire.js");
1908
- const questions = generatePersonaQuestionnaire({
1909
- comprehensive,
1910
- traits: traits,
1911
- });
1912
- const formatted = formatForAskUserQuestion(questions);
1913
- return {
1914
- content: [
1915
- {
1916
- type: "text",
1917
- text: JSON.stringify({
1918
- instructions: "Present these questions to the user one at a time or all at once. Each answer maps to a trait value. After collecting answers, use persona_questionnaire_build to create the persona. v16.12.0: Start with the category question to enable disability-aware value safeguards.",
1919
- questionCount: questions.length,
1920
- questions: formatted,
1921
- rawQuestions: questions, // Include raw for programmatic use
1922
- ...(includeCategory && {
1923
- categoryQuestion: CATEGORY_QUESTION,
1924
- categoryInstructions: "Ask this FIRST to determine persona category. The category affects which values are applied and provides research-based safeguards for disability simulations.",
1925
- }),
1926
- }, null, 2),
1927
- },
1928
- ],
1929
- };
1930
- });
1931
- server.tool("persona_questionnaire_build", "Build a custom persona from questionnaire answers with category-aware value safeguards. Answers should be a map of trait names to values (0-1). Missing traits will use intelligent defaults based on research correlations. v16.12.0: Optionally specify category for disability-specific value handling.", {
1932
- name: z.string().describe("Name for the new persona"),
1933
- description: z.string().describe("Description of the persona"),
1934
- answers: z.record(z.string(), z.number()).describe("Map of trait names to values (0-1), e.g. {patience: 0.25, riskTolerance: 0.75}"),
1935
- category: z.enum(["cognitive", "physical", "sensory", "emotional", "general"]).optional().describe("Persona category for value safeguards (v16.12.0)"),
1936
- valueOverrides: z.record(z.string(), z.number()).optional().describe("Override specific values (0-1) if different from category defaults"),
1937
- save: z.boolean().optional().default(true).describe("Save the persona to disk for future use"),
1938
- }, async ({ name, description, answers, category, valueOverrides, save }) => {
1939
- const { buildTraitsFromAnswers, getTraitLabel, getTraitBehaviors, detectPersonaCategory, buildValuesFromCategory, validateCategoryValues, } = await import("./persona-questionnaire.js");
1940
- const { createCognitivePersona, saveCustomPersona } = await import("./personas.js");
1941
- // Detect or use provided category
1942
- const detectedCategory = category || detectPersonaCategory(name, description);
1943
- // Build traits from answers with research-based correlations (moved up for v16.14.0)
1944
- const traits = buildTraitsFromAnswers(answers);
1945
- // Build category-appropriate values with optional overrides
1946
- // v16.14.0: Pass traits for trait_based categories (general, emotional)
1947
- const categoryResult = buildValuesFromCategory(detectedCategory, valueOverrides, traits // v16.14.0: Pass traits for trait-based value derivation
1948
- );
1949
- // Validate values match category guidelines
1950
- const warnings = validateCategoryValues(detectedCategory, categoryResult.values);
1951
- // Create the persona
1952
- const persona = createCognitivePersona(name, description, traits, {});
1953
- // Save if requested
1954
- let savedPath;
1955
- if (save) {
1956
- savedPath = saveCustomPersona(persona);
1957
- }
1958
- // Generate behavioral summary for key traits
1959
- const traitSummary = {};
1960
- for (const [trait, value] of Object.entries(traits)) {
1961
- if (value !== 0.5) { // Only include non-default traits
1962
- traitSummary[trait] = {
1963
- value: value,
1964
- label: getTraitLabel(trait, value),
1965
- behaviors: getTraitBehaviors(trait, value),
1966
- };
1967
- }
1968
- }
1969
- return {
1970
- content: [
1971
- {
1972
- type: "text",
1973
- text: JSON.stringify({
1974
- success: true,
1975
- persona: {
1976
- name: persona.name,
1977
- description: persona.description,
1978
- demographics: persona.demographics,
1979
- },
1980
- cognitiveTraits: traits,
1981
- traitSummary,
1982
- // v16.12.0: Category-aware values
1983
- category: {
1984
- detected: detectedCategory,
1985
- strategy: categoryResult.valueStrategy,
1986
- guidance: categoryResult.guidance,
1987
- },
1988
- values: categoryResult.values,
1989
- researchBasis: categoryResult.researchBasis,
1990
- // v16.14.0: Show how traits influenced values for trait_based categories
1991
- ...(categoryResult.derivations && categoryResult.derivations.length > 0 && {
1992
- valueDerivations: categoryResult.derivations,
1993
- }),
1994
- ...(warnings.length > 0 && { warnings }),
1995
- savedPath,
1996
- usage: `Use persona "${name}" with cognitive-journey or other commands`,
1997
- }, null, 2),
1998
- },
1999
- ],
2000
- };
2001
- });
2002
- server.tool("persona_trait_lookup", "Look up behavioral descriptions for specific trait values. Useful for understanding what a trait value means in practice.", {
2003
- trait: z.string().describe("Trait name (e.g., 'patience', 'riskTolerance')"),
2004
- value: z.number().min(0).max(1).describe("Trait value (0-1)"),
2005
- }, async ({ trait, value }) => {
2006
- const { getTraitReference, getTraitLabel, getTraitBehaviors } = await import("./persona-questionnaire.js");
2007
- const reference = getTraitReference(trait);
2008
- if (!reference) {
2009
- return {
2010
- content: [
2011
- {
2012
- type: "text",
2013
- text: JSON.stringify({
2014
- error: `Unknown trait: ${trait}`,
2015
- availableTraits: [
2016
- "patience", "riskTolerance", "comprehension", "persistence", "curiosity",
2017
- "workingMemory", "readingTendency", "resilience", "selfEfficacy", "satisficing",
2018
- "trustCalibration", "interruptRecovery", "informationForaging", "changeBlindness",
2019
- "anchoringBias", "timeHorizon", "attributionStyle", "metacognitivePlanning",
2020
- "proceduralFluency", "transferLearning", "authoritySensitivity", "emotionalContagion",
2021
- "fearOfMissingOut", "socialProofSensitivity", "mentalModelRigidity"
2022
- ],
2023
- }, null, 2),
2024
- },
2025
- ],
2026
- };
2027
- }
2028
- return {
2029
- content: [
2030
- {
2031
- type: "text",
2032
- text: JSON.stringify({
2033
- trait: reference.name,
2034
- description: reference.description,
2035
- researchBasis: reference.researchBasis,
2036
- value,
2037
- label: getTraitLabel(trait, value),
2038
- behaviors: getTraitBehaviors(trait, value),
2039
- allLevels: reference.levels,
2040
- }, null, 2),
2041
- },
2042
- ],
2043
- };
2044
- });
2045
- server.tool("persona_category_guidance", "Get guidance for value assignment based on persona category. (v16.12.0) Explains research basis for why cognitive, physical, sensory, and emotional disability categories require different value handling approaches.", {
2046
- category: z.enum(["cognitive", "physical", "sensory", "emotional", "general"]).describe("Persona category to get guidance for"),
2047
- }, async ({ category }) => {
2048
- const { CATEGORY_VALUE_PRESETS, COGNITIVE_SUBTYPES } = await import("./persona-questionnaire.js");
2049
- const preset = CATEGORY_VALUE_PRESETS.find(p => p.category === category);
2050
- if (!preset) {
2051
- return {
2052
- content: [
2053
- {
2054
- type: "text",
2055
- text: JSON.stringify({
2056
- error: `Unknown category: ${category}`,
2057
- availableCategories: ["cognitive", "physical", "sensory", "emotional", "general"],
2058
- }, null, 2),
2059
- },
2060
- ],
2061
- };
2062
- }
2063
- return {
2064
- content: [
2065
- {
2066
- type: "text",
2067
- text: JSON.stringify({
2068
- category: preset.category,
2069
- description: preset.description,
2070
- valueStrategy: preset.valueStrategy,
2071
- guidance: preset.guidance,
2072
- defaultValues: preset.defaultValues,
2073
- researchBasis: preset.researchBasis,
2074
- ...(category === "cognitive" && {
2075
- subtypes: Object.entries(COGNITIVE_SUBTYPES).map(([key, subtype]) => ({
2076
- name: key,
2077
- values: subtype.values,
2078
- researchBasis: subtype.researchBasis,
2079
- })),
2080
- }),
2081
- }, null, 2),
2082
- },
2083
- ],
2084
- };
2085
- });
2086
- // =========================================================================
2087
- // Performance Tools (v6.4.0+)
2088
- // =========================================================================
2089
- server.tool("perf_baseline", "Capture performance baseline for a URL", {
2090
- url: z.string().url().describe("URL to capture baseline for"),
2091
- name: z.string().describe("Name for the baseline"),
2092
- runs: z.number().optional().default(3).describe("Number of runs to average"),
2093
- }, async ({ url, name, runs }) => {
2094
- const result = await capturePerformanceBaseline(url, { name, runs });
2095
- // v16.11.0: Return all available metrics, not just core 4
2096
- const m = result.metrics;
2097
- return {
2098
- content: [
2099
- {
2100
- type: "text",
2101
- text: JSON.stringify({
2102
- name: result.name,
2103
- url: result.url,
2104
- // Core Web Vitals
2105
- coreWebVitals: {
2106
- lcp: m.lcp,
2107
- lcpRating: m.lcpRating,
2108
- fid: m.fid,
2109
- fidRating: m.fidRating,
2110
- cls: m.cls,
2111
- clsRating: m.clsRating,
2112
- },
2113
- // Additional timing metrics
2114
- timingMetrics: {
2115
- fcp: m.fcp,
2116
- fcpRating: m.fcpRating,
2117
- ttfb: m.ttfb,
2118
- ttfbRating: m.ttfbRating,
2119
- tti: m.tti,
2120
- tbt: m.tbt,
2121
- domContentLoaded: m.domContentLoaded,
2122
- load: m.load,
2123
- },
2124
- // Resource metrics
2125
- resourceMetrics: {
2126
- resourceCount: m.resourceCount,
2127
- transferSize: m.transferSize,
2128
- },
2129
- // Flat copy for backward compatibility
2130
- metrics: {
2131
- lcp: m.lcp,
2132
- fcp: m.fcp,
2133
- ttfb: m.ttfb,
2134
- cls: m.cls,
2135
- },
2136
- }, null, 2),
2137
- },
2138
- ],
2139
- };
2140
- });
2141
- server.tool("perf_regression", "Detect performance regression against baseline with configurable sensitivity. Uses dual thresholds: both percentage AND absolute change must be exceeded. Profiles: strict (perf envs, FCP 10%/50ms), normal (default, FCP 20%/100ms), ci (automated pipelines, FCP 25%/150ms), lenient (dev, FCP 30%/200ms).", {
2142
- url: z.string().url().describe("URL to test"),
2143
- baselineName: z.string().describe("Name of baseline to compare against"),
2144
- sensitivity: z.enum(["strict", "normal", "ci", "lenient"]).optional().default("normal").describe("Sensitivity profile: strict (perf testing), normal (local dev), ci (automated pipelines), lenient (development)"),
2145
- thresholdLcp: z.number().optional().describe("Override LCP threshold percentage"),
2146
- }, async ({ url, baselineName, sensitivity, thresholdLcp }) => {
2147
- const result = await detectPerformanceRegression(url, baselineName, {
2148
- sensitivity,
2149
- thresholds: thresholdLcp ? { lcp: thresholdLcp } : undefined,
2150
- });
2151
- return {
2152
- content: [
2153
- {
2154
- type: "text",
2155
- text: JSON.stringify({
2156
- passed: result.passed,
2157
- sensitivity: result.sensitivity,
2158
- notes: result.notes,
2159
- regressions: result.regressions,
2160
- currentMetrics: result.currentMetrics,
2161
- baseline: result.baseline.name,
2162
- }, null, 2),
2163
- },
2164
- ],
2165
- };
2166
- });
2167
- server.tool("list_baselines", "List all saved baselines (visual and performance)", {}, async () => {
2168
- const visualBaselines = await listVisualBaselines();
2169
- const perfBaselines = await listPerformanceBaselines();
2170
- return {
2171
- content: [
2172
- {
2173
- type: "text",
2174
- text: JSON.stringify({
2175
- visual: visualBaselines,
2176
- performance: perfBaselines,
2177
- }, null, 2),
2178
- },
2179
- ],
2180
- };
2181
- });
2182
- // =========================================================================
2183
- // Agent-Ready Audit, Competitive Benchmark, Accessibility Empathy (v9.0.0)
2184
- // =========================================================================
2185
- server.tool("agent_ready_audit", "Audit a website for AI-agent friendliness. Analyzes findability, stability, accessibility, and semantics. Returns score (0-100), grade (A-F), issues, and remediation recommendations.", {
2186
- url: z.string().url().describe("URL to audit"),
2187
- }, async ({ url }) => {
2188
- const result = await runAgentReadyAudit(url, { headless: true });
2189
- return {
2190
- content: [
2191
- {
2192
- type: "text",
2193
- text: JSON.stringify({
2194
- url: result.url,
2195
- score: result.score,
2196
- grade: result.grade,
2197
- summary: result.summary,
2198
- topIssues: result.issues.slice(0, 5),
2199
- topRecommendations: result.recommendations.slice(0, 5),
2200
- duration: result.duration,
2201
- }, null, 2),
2202
- },
2203
- ],
2204
- };
2205
- });
2206
- server.tool("competitive_benchmark", "Compare UX across competitor sites. Runs identical cognitive journeys on multiple sites and generates head-to-head comparison with rankings, friction analysis, and recommendations.", {
2207
- sites: z.array(z.string().url()).describe("Array of URLs to compare"),
2208
- goal: z.string().describe("Task goal (e.g., 'sign up for free trial')"),
2209
- persona: z.string().optional().default("first-timer").describe("Persona to use"),
2210
- maxSteps: z.number().optional().default(30).describe("Max steps per site"),
2211
- maxTime: z.number().optional().default(180).describe("Max time per site in seconds"),
2212
- }, async ({ sites, goal, persona, maxSteps, maxTime }) => {
2213
- const result = await runCompetitiveBenchmark({
2214
- sites: sites.map((url) => ({ url })),
2215
- goal,
2216
- persona,
2217
- maxSteps,
2218
- maxTime,
2219
- headless: true,
2220
- });
2221
- return {
2222
- content: [
2223
- {
2224
- type: "text",
2225
- text: JSON.stringify({
2226
- goal: result.goal,
2227
- persona: result.persona,
2228
- ranking: result.ranking,
2229
- comparison: result.comparison,
2230
- recommendations: result.recommendations.slice(0, 5),
2231
- duration: result.duration,
2232
- }, null, 2),
2233
- },
2234
- ],
2235
- };
2236
- });
2237
- server.tool("empathy_audit", "Simulate how people with disabilities experience a site. Tests motor impairments, cognitive differences, and sensory limitations. Returns barriers, WCAG violations, and remediation suggestions.", {
2238
- url: z.string().url().describe("URL to audit"),
2239
- goal: z.string().describe("Task goal (e.g., 'complete checkout')"),
2240
- disabilities: z.array(z.string()).optional().describe("Disability personas to test. Available: motor-impairment-tremor, low-vision-magnified, cognitive-adhd, dyslexic-user, deaf-user, elderly-low-vision, color-blind-deuteranopia"),
2241
- wcagLevel: z.enum(["A", "AA", "AAA"]).optional().default("AA").describe("WCAG conformance level"),
2242
- maxSteps: z.number().optional().default(20).describe("Max steps per persona"),
2243
- maxTime: z.number().optional().default(120).describe("Max time per persona in seconds"),
2244
- }, async ({ url, goal, disabilities, wcagLevel, maxSteps, maxTime }) => {
2245
- const disabilityList = disabilities || listAccessibilityPersonas();
2246
- const result = await runEmpathyAudit(url, {
2247
- goal,
2248
- disabilities: disabilityList,
2249
- wcagLevel,
2250
- maxSteps,
2251
- maxTime,
2252
- headless: true,
2253
- });
2254
- return {
2255
- content: [
2256
- {
2257
- type: "text",
2258
- text: JSON.stringify({
2259
- url: result.url,
2260
- goal: result.goal,
2261
- overallScore: result.overallScore,
2262
- resultsSummary: result.results.map((r) => {
2263
- // v16.7.2: Separate barrier types from element counts
2264
- const uniqueTypes = new Set(r.barriers.map(b => b.type));
2265
- return {
2266
- persona: r.persona,
2267
- disabilityType: r.disabilityType,
2268
- goalAchieved: r.goalAchieved,
2269
- empathyScore: r.empathyScore,
2270
- barrierTypeCount: uniqueTypes.size, // Unique barrier categories
2271
- barrierTypes: Array.from(uniqueTypes),
2272
- affectedElements: r.barriers.length, // Raw element count
2273
- wcagViolationCount: r.wcagViolations.length,
2274
- };
2275
- }),
2276
- allWcagViolations: result.allWcagViolations,
2277
- topBarriers: result.topBarriers.slice(0, 5), // v11.11.0: Deduplicated by type
2278
- topRemediation: result.combinedRemediation.slice(0, 5),
2279
- duration: result.duration,
2280
- }, null, 2),
2281
- },
2282
- ],
2283
- };
2284
- });
2285
- // Diagnostics
2286
- server.tool("status", "Get CBrowser environment status and diagnostics including data directories, installed browsers, configuration, and self-healing cache statistics", {}, async () => {
2287
- const info = await getStatusInfo(VERSION);
2288
- return {
2289
- content: [
2290
- {
2291
- type: "text",
2292
- text: JSON.stringify(info, null, 2),
2293
- },
2294
- ],
2295
- };
2296
- });
2297
- // =========================================================================
2298
- // Browser Management Tools (v11.8.0)
2299
- // =========================================================================
2300
- server.tool("browser_health", "Check if the browser is healthy and responsive. Use this before operations if you suspect the browser may have crashed.", {}, async () => {
2301
- const b = await getBrowser();
2302
- const result = await b.isBrowserHealthy();
2303
- return {
2304
- content: [
2305
- {
2306
- type: "text",
2307
- text: JSON.stringify(result, null, 2),
2308
- },
2309
- ],
2310
- };
2311
- });
2312
- server.tool("browser_recover", "Attempt to recover from a browser crash by restarting the browser process. Use this when browser_health returns unhealthy.", {
2313
- restoreUrl: z.string().url().optional().describe("URL to restore after recovery (uses last known URL if not provided)"),
2314
- maxAttempts: z.number().optional().default(3).describe("Maximum recovery attempts"),
2315
- }, async ({ restoreUrl, maxAttempts }) => {
2316
- const b = await getBrowser();
2317
- const result = await b.recoverBrowser({ restoreUrl, maxAttempts });
2318
- return {
2319
- content: [
2320
- {
2321
- type: "text",
2322
- text: JSON.stringify(result, null, 2),
2323
- },
2324
- ],
2325
- };
2326
- });
2327
- server.tool("reset_browser", "Reset the browser to a clean state. Clears all cookies, localStorage, sessionStorage, and browser state. Use this when you need a fresh browser environment.", {}, async () => {
2328
- const b = await getBrowser();
2329
- await b.reset();
2330
- // Relaunch for immediate use
2331
- await b.launch();
2332
- return {
2333
- content: [
2334
- {
2335
- type: "text",
2336
- text: JSON.stringify({
2337
- success: true,
2338
- message: "Browser reset to clean state and relaunched",
2339
- }, null, 2),
2340
- },
2341
- ],
2342
- };
2343
- });
2344
- // ============================================================================
2345
- // NOTE: Stealth tools moved to Enterprise (v16.18.0)
2346
- // The following tools are now Enterprise-only:
2347
- // - stealth_status, stealth_enable, stealth_disable, stealth_check, stealth_diagnose
2348
- // - cloudflare_detect, cloudflare_wait
2349
- // Contact alexandria.shai.eden@gmail.com for Enterprise access.
2350
- // ============================================================================
223
+ function configureMcpTools(server, customRegisterTools) {
224
+ const context = { getBrowser };
225
+ if (customRegisterTools) {
226
+ // Use custom tool registration (for Enterprise servers)
227
+ customRegisterTools(server, context);
228
+ }
229
+ else {
230
+ // Register all public npm tools (82 total: 60 real + 22 enterprise stubs)
231
+ registerAllPublicTools(server, context);
232
+ }
2351
233
  }
2352
234
  /**
2353
235
  * Create a configured MCP server instance
2354
236
  */
2355
- function createMcpServer() {
237
+ function createMcpServer(customRegisterTools) {
2356
238
  const server = new McpServer({
2357
239
  name: "cbrowser",
2358
240
  version: VERSION,
2359
241
  });
2360
- configureMcpTools(server);
242
+ configureMcpTools(server, customRegisterTools);
2361
243
  return server;
2362
244
  }
2363
245
  /**
2364
246
  * Handle incoming HTTP MCP request
2365
247
  */
2366
248
  async function handleMcpRequest(req, res, transport) {
2367
- // CORS headers are set at the top level in startRemoteMcpServer
2368
249
  const start = Date.now();
2369
250
  // Parse body for POST requests
2370
251
  if (req.method === "POST") {
@@ -2440,7 +321,6 @@ export async function startRemoteMcpServer(options) {
2440
321
  const httpServer = createServer(async (req, res) => {
2441
322
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
2442
323
  // CORS headers for all responses
2443
- // Use request origin for CORS to avoid wildcard security issues
2444
324
  const origin = req.headers.origin;
2445
325
  if (origin) {
2446
326
  res.setHeader("Access-Control-Allow-Origin", origin);
@@ -2529,8 +409,8 @@ export async function startRemoteMcpServer(options) {
2529
409
  transport = new StreamableHTTPServerTransport({
2530
410
  sessionIdGenerator: sessionMode === "stateful" ? () => randomUUID() : undefined,
2531
411
  });
2532
- // Create and connect server
2533
- const server = createMcpServer();
412
+ // Create and connect server (with optional custom tool registration)
413
+ const server = createMcpServer(options?.registerTools);
2534
414
  // Allow extension with additional tools (for Enterprise)
2535
415
  if (options?.extendServer) {
2536
416
  await options.extendServer(server);