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.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server-remote.d.ts +8 -0
- package/dist/mcp-server-remote.d.ts.map +1 -1
- package/dist/mcp-server-remote.js +94 -2214
- package/dist/mcp-server-remote.js.map +1 -1
- package/dist/mcp-tools/ask-user-tools.d.ts +12 -0
- package/dist/mcp-tools/ask-user-tools.d.ts.map +1 -0
- package/dist/mcp-tools/ask-user-tools.js +60 -0
- package/dist/mcp-tools/ask-user-tools.js.map +1 -0
- package/dist/mcp-tools/base/analysis-tools.d.ts +12 -0
- package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/analysis-tools.js +70 -0
- package/dist/mcp-tools/base/analysis-tools.js.map +1 -0
- package/dist/mcp-tools/base/assertion-tools.d.ts +12 -0
- package/dist/mcp-tools/base/assertion-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/assertion-tools.js +32 -0
- package/dist/mcp-tools/base/assertion-tools.js.map +1 -0
- package/dist/mcp-tools/base/audit-tools.d.ts +12 -0
- package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/audit-tools.js +114 -0
- package/dist/mcp-tools/base/audit-tools.js.map +1 -0
- package/dist/mcp-tools/base/browser-management-tools.d.ts +12 -0
- package/dist/mcp-tools/base/browser-management-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/browser-management-tools.js +69 -0
- package/dist/mcp-tools/base/browser-management-tools.js.map +1 -0
- package/dist/mcp-tools/base/bug-analysis-tools.d.ts +12 -0
- package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/bug-analysis-tools.js +97 -0
- package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -0
- package/dist/mcp-tools/base/cognitive-tools.d.ts +12 -0
- package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/cognitive-tools.js +453 -0
- package/dist/mcp-tools/base/cognitive-tools.js.map +1 -0
- package/dist/mcp-tools/base/extraction-tools.d.ts +12 -0
- package/dist/mcp-tools/base/extraction-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/extraction-tools.js +41 -0
- package/dist/mcp-tools/base/extraction-tools.js.map +1 -0
- package/dist/mcp-tools/base/healing-tools.d.ts +12 -0
- package/dist/mcp-tools/base/healing-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/healing-tools.js +24 -0
- package/dist/mcp-tools/base/healing-tools.js.map +1 -0
- package/dist/mcp-tools/base/index.d.ts +49 -0
- package/dist/mcp-tools/base/index.d.ts.map +1 -0
- package/dist/mcp-tools/base/index.js +99 -0
- package/dist/mcp-tools/base/index.js.map +1 -0
- package/dist/mcp-tools/base/interaction-tools.d.ts +12 -0
- package/dist/mcp-tools/base/interaction-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/interaction-tools.js +168 -0
- package/dist/mcp-tools/base/interaction-tools.js.map +1 -0
- package/dist/mcp-tools/base/navigation-tools.d.ts +12 -0
- package/dist/mcp-tools/base/navigation-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/navigation-tools.js +33 -0
- package/dist/mcp-tools/base/navigation-tools.js.map +1 -0
- package/dist/mcp-tools/base/performance-tools.d.ts +12 -0
- package/dist/mcp-tools/base/performance-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/performance-tools.js +103 -0
- package/dist/mcp-tools/base/performance-tools.js.map +1 -0
- package/dist/mcp-tools/base/persona-comparison-tools.d.ts +12 -0
- package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/persona-comparison-tools.js +242 -0
- package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -0
- package/dist/mcp-tools/base/session-tools.d.ts +12 -0
- package/dist/mcp-tools/base/session-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/session-tools.js +67 -0
- package/dist/mcp-tools/base/session-tools.js.map +1 -0
- package/dist/mcp-tools/base/testing-tools.d.ts +12 -0
- package/dist/mcp-tools/base/testing-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/testing-tools.js +199 -0
- package/dist/mcp-tools/base/testing-tools.js.map +1 -0
- package/dist/mcp-tools/base/values-tools.d.ts +13 -0
- package/dist/mcp-tools/base/values-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/values-tools.js +290 -0
- package/dist/mcp-tools/base/values-tools.js.map +1 -0
- package/dist/mcp-tools/base/visual-testing-tools.d.ts +12 -0
- package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/visual-testing-tools.js +159 -0
- package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -0
- package/dist/mcp-tools/enterprise-stubs.d.ts +23 -0
- package/dist/mcp-tools/enterprise-stubs.d.ts.map +1 -0
- package/dist/mcp-tools/enterprise-stubs.js +238 -0
- package/dist/mcp-tools/enterprise-stubs.js.map +1 -0
- package/dist/mcp-tools/index.d.ts +36 -0
- package/dist/mcp-tools/index.d.ts.map +1 -0
- package/dist/mcp-tools/index.js +52 -0
- package/dist/mcp-tools/index.js.map +1 -0
- package/dist/mcp-tools/persona-creation-tools.d.ts +21 -0
- package/dist/mcp-tools/persona-creation-tools.d.ts.map +1 -0
- package/dist/mcp-tools/persona-creation-tools.js +528 -0
- package/dist/mcp-tools/persona-creation-tools.js.map +1 -0
- package/dist/mcp-tools/types.d.ts +24 -0
- package/dist/mcp-tools/types.d.ts.map +1 -0
- package/dist/mcp-tools/types.js +8 -0
- package/dist/mcp-tools/types.js.map +1 -0
- 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
|
|
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
|
|
64
|
+
// Transport storage for stateful sessions
|
|
91
65
|
const transports = new Map();
|
|
92
|
-
|
|
93
|
-
// Token cache to avoid hitting Auth0 rate limits
|
|
66
|
+
// Token cache for Auth0 validation
|
|
94
67
|
const tokenCache = new Map();
|
|
95
|
-
const
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
120
|
-
if (!
|
|
82
|
+
const auth0 = getAuth0Config();
|
|
83
|
+
if (!auth0)
|
|
121
84
|
return null;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
|
178
|
-
if (!
|
|
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:
|
|
184
|
-
authorization_servers: [`https://${
|
|
139
|
+
resource: auth0.audience,
|
|
140
|
+
authorization_servers: [`https://${auth0.domain}`],
|
|
185
141
|
bearer_methods_supported: ["header"],
|
|
186
|
-
scopes_supported: ["openid", "profile", "
|
|
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;
|
|
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(",")
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
241
|
-
if (auth0Enabled
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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);
|