cbrowser 7.4.1 → 7.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +159 -46
  2. package/dist/analysis/bug-hunter.d.ts +0 -0
  3. package/dist/analysis/bug-hunter.d.ts.map +0 -0
  4. package/dist/analysis/bug-hunter.js +0 -0
  5. package/dist/analysis/bug-hunter.js.map +0 -0
  6. package/dist/analysis/chaos-testing.d.ts +0 -0
  7. package/dist/analysis/chaos-testing.d.ts.map +0 -0
  8. package/dist/analysis/chaos-testing.js +0 -0
  9. package/dist/analysis/chaos-testing.js.map +0 -0
  10. package/dist/analysis/index.d.ts +0 -0
  11. package/dist/analysis/index.d.ts.map +0 -0
  12. package/dist/analysis/index.js +0 -0
  13. package/dist/analysis/index.js.map +0 -0
  14. package/dist/analysis/natural-language.d.ts +0 -0
  15. package/dist/analysis/natural-language.d.ts.map +0 -0
  16. package/dist/analysis/natural-language.js +0 -0
  17. package/dist/analysis/natural-language.js.map +0 -0
  18. package/dist/analysis/persona-comparison.d.ts +0 -0
  19. package/dist/analysis/persona-comparison.d.ts.map +0 -0
  20. package/dist/analysis/persona-comparison.js +0 -0
  21. package/dist/analysis/persona-comparison.js.map +0 -0
  22. package/dist/browser.d.ts +0 -0
  23. package/dist/browser.d.ts.map +0 -0
  24. package/dist/browser.js +0 -0
  25. package/dist/browser.js.map +0 -0
  26. package/dist/cli.d.ts +0 -0
  27. package/dist/cli.d.ts.map +0 -0
  28. package/dist/cli.js +17 -1
  29. package/dist/cli.js.map +1 -1
  30. package/dist/config.d.ts +0 -0
  31. package/dist/config.d.ts.map +0 -0
  32. package/dist/config.js +0 -0
  33. package/dist/config.js.map +0 -0
  34. package/dist/daemon.d.ts +0 -0
  35. package/dist/daemon.d.ts.map +0 -0
  36. package/dist/daemon.js +0 -0
  37. package/dist/daemon.js.map +0 -0
  38. package/dist/index.d.ts +0 -0
  39. package/dist/index.d.ts.map +0 -0
  40. package/dist/index.js +0 -0
  41. package/dist/index.js.map +0 -0
  42. package/dist/mcp-server-remote.d.ts +23 -0
  43. package/dist/mcp-server-remote.d.ts.map +1 -0
  44. package/dist/mcp-server-remote.js +890 -0
  45. package/dist/mcp-server-remote.js.map +1 -0
  46. package/dist/mcp-server.d.ts +0 -0
  47. package/dist/mcp-server.d.ts.map +0 -0
  48. package/dist/mcp-server.js +1 -1
  49. package/dist/mcp-server.js.map +0 -0
  50. package/dist/performance/index.d.ts +0 -0
  51. package/dist/performance/index.d.ts.map +0 -0
  52. package/dist/performance/index.js +0 -0
  53. package/dist/performance/index.js.map +0 -0
  54. package/dist/performance/metrics.d.ts +0 -0
  55. package/dist/performance/metrics.d.ts.map +0 -0
  56. package/dist/performance/metrics.js +0 -0
  57. package/dist/performance/metrics.js.map +0 -0
  58. package/dist/personas.d.ts +0 -0
  59. package/dist/personas.d.ts.map +0 -0
  60. package/dist/personas.js +0 -0
  61. package/dist/personas.js.map +0 -0
  62. package/dist/testing/coverage.d.ts +0 -0
  63. package/dist/testing/coverage.d.ts.map +0 -0
  64. package/dist/testing/coverage.js +0 -0
  65. package/dist/testing/coverage.js.map +0 -0
  66. package/dist/testing/flaky-detection.d.ts +0 -0
  67. package/dist/testing/flaky-detection.d.ts.map +0 -0
  68. package/dist/testing/flaky-detection.js +0 -0
  69. package/dist/testing/flaky-detection.js.map +0 -0
  70. package/dist/testing/index.d.ts +0 -0
  71. package/dist/testing/index.d.ts.map +0 -0
  72. package/dist/testing/index.js +0 -0
  73. package/dist/testing/index.js.map +0 -0
  74. package/dist/testing/nl-test-suite.d.ts +0 -0
  75. package/dist/testing/nl-test-suite.d.ts.map +0 -0
  76. package/dist/testing/nl-test-suite.js +0 -0
  77. package/dist/testing/nl-test-suite.js.map +0 -0
  78. package/dist/testing/test-repair.d.ts +0 -0
  79. package/dist/testing/test-repair.d.ts.map +0 -0
  80. package/dist/testing/test-repair.js +0 -0
  81. package/dist/testing/test-repair.js.map +0 -0
  82. package/dist/types.d.ts +0 -0
  83. package/dist/types.d.ts.map +0 -0
  84. package/dist/types.js +0 -0
  85. package/dist/types.js.map +0 -0
  86. package/dist/visual/ab-comparison.d.ts +0 -0
  87. package/dist/visual/ab-comparison.d.ts.map +1 -1
  88. package/dist/visual/ab-comparison.js +3 -2
  89. package/dist/visual/ab-comparison.js.map +1 -1
  90. package/dist/visual/cross-browser.d.ts +0 -0
  91. package/dist/visual/cross-browser.d.ts.map +1 -1
  92. package/dist/visual/cross-browser.js +2 -1
  93. package/dist/visual/cross-browser.js.map +1 -1
  94. package/dist/visual/index.d.ts +0 -0
  95. package/dist/visual/index.d.ts.map +0 -0
  96. package/dist/visual/index.js +0 -0
  97. package/dist/visual/index.js.map +0 -0
  98. package/dist/visual/regression.d.ts +0 -0
  99. package/dist/visual/regression.d.ts.map +1 -1
  100. package/dist/visual/regression.js +2 -1
  101. package/dist/visual/regression.js.map +1 -1
  102. package/dist/visual/responsive.d.ts +0 -0
  103. package/dist/visual/responsive.d.ts.map +1 -1
  104. package/dist/visual/responsive.js +3 -2
  105. package/dist/visual/responsive.js.map +1 -1
  106. package/docs/REMOTE-MCP-SERVER.md +520 -0
  107. package/package.json +6 -1
@@ -0,0 +1,890 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * CBrowser Remote MCP Server
5
+ *
6
+ * HTTP-based MCP server for remote access via claude.ai custom connectors.
7
+ * Uses StreamableHTTPServerTransport for HTTP/SSE communication.
8
+ *
9
+ * Run with: cbrowser mcp-remote
10
+ * Or: npx cbrowser mcp-remote
11
+ * Or: node dist/mcp-server-remote.js
12
+ *
13
+ * Environment variables:
14
+ * PORT - Port to listen on (default: 3000)
15
+ * HOST - Host to bind to (default: 0.0.0.0)
16
+ * MCP_SESSION_MODE - 'stateful' or 'stateless' (default: stateless)
17
+ * MCP_API_KEY - API key for authentication (optional, if not set server is open)
18
+ * MCP_API_KEYS - Comma-separated list of valid API keys (optional, alternative to MCP_API_KEY)
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.startRemoteMcpServer = startRemoteMcpServer;
22
+ const node_http_1 = require("node:http");
23
+ const node_crypto_1 = require("node:crypto");
24
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
25
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
26
+ const zod_1 = require("zod");
27
+ const browser_js_1 = require("./browser.js");
28
+ // Visual module imports
29
+ const index_js_1 = require("./visual/index.js");
30
+ // Testing module imports
31
+ const index_js_2 = require("./testing/index.js");
32
+ // Analysis module imports
33
+ const index_js_3 = require("./analysis/index.js");
34
+ // Performance module imports
35
+ const index_js_4 = require("./performance/index.js");
36
+ // Shared browser instance
37
+ let browser = null;
38
+ async function getBrowser() {
39
+ if (!browser) {
40
+ browser = new browser_js_1.CBrowser({
41
+ headless: true,
42
+ persistent: true,
43
+ });
44
+ }
45
+ return browser;
46
+ }
47
+ // Transport instances by session (for stateful mode)
48
+ const transports = new Map();
49
+ /**
50
+ * Get configured API keys from environment
51
+ */
52
+ function getApiKeys() {
53
+ const singleKey = process.env.MCP_API_KEY;
54
+ const multipleKeys = process.env.MCP_API_KEYS;
55
+ if (!singleKey && !multipleKeys) {
56
+ return null; // No authentication configured
57
+ }
58
+ const keys = new Set();
59
+ if (singleKey) {
60
+ keys.add(singleKey);
61
+ }
62
+ if (multipleKeys) {
63
+ multipleKeys.split(",").map(k => k.trim()).filter(k => k).forEach(k => keys.add(k));
64
+ }
65
+ return keys;
66
+ }
67
+ /**
68
+ * Validate API key from request headers
69
+ * Supports: Authorization: Bearer <key> or X-API-Key: <key>
70
+ */
71
+ function validateApiKey(req, validKeys) {
72
+ // Check Authorization header (Bearer token)
73
+ const authHeader = req.headers.authorization;
74
+ if (authHeader?.startsWith("Bearer ")) {
75
+ const token = authHeader.slice(7);
76
+ if (validKeys.has(token)) {
77
+ return true;
78
+ }
79
+ }
80
+ // Check X-API-Key header
81
+ const apiKeyHeader = req.headers["x-api-key"];
82
+ if (typeof apiKeyHeader === "string" && validKeys.has(apiKeyHeader)) {
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+ /**
88
+ * Send 401 Unauthorized response
89
+ */
90
+ function sendUnauthorized(res) {
91
+ res.writeHead(401, {
92
+ "Content-Type": "application/json",
93
+ "WWW-Authenticate": "Bearer realm=\"cbrowser-mcp\""
94
+ });
95
+ res.end(JSON.stringify({
96
+ error: "Unauthorized",
97
+ message: "Valid API key required. Use Authorization: Bearer <key> or X-API-Key: <key>"
98
+ }));
99
+ }
100
+ /**
101
+ * Configure all CBrowser tools on an MCP server instance.
102
+ * This is shared between stdio and HTTP transports.
103
+ */
104
+ function configureMcpTools(server) {
105
+ // =========================================================================
106
+ // Navigation Tools
107
+ // =========================================================================
108
+ server.tool("navigate", "Navigate to a URL and take a screenshot", {
109
+ url: zod_1.z.string().url().describe("The URL to navigate to"),
110
+ }, async ({ url }) => {
111
+ const b = await getBrowser();
112
+ const result = await b.navigate(url);
113
+ return {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text: JSON.stringify({
118
+ success: true,
119
+ url: result.url,
120
+ title: result.title,
121
+ loadTime: result.loadTime,
122
+ screenshot: result.screenshot,
123
+ }, null, 2),
124
+ },
125
+ ],
126
+ };
127
+ });
128
+ // =========================================================================
129
+ // Interaction Tools
130
+ // =========================================================================
131
+ server.tool("click", "Click an element on the page using text, selector, or description", {
132
+ selector: zod_1.z.string().describe("Element to click (text content, CSS selector, or description)"),
133
+ force: zod_1.z.boolean().optional().describe("Bypass safety checks for destructive actions"),
134
+ }, async ({ selector, force }) => {
135
+ const b = await getBrowser();
136
+ const result = await b.click(selector, { force });
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: JSON.stringify({
142
+ success: result.success,
143
+ message: result.message,
144
+ screenshot: result.screenshot,
145
+ }, null, 2),
146
+ },
147
+ ],
148
+ };
149
+ });
150
+ server.tool("smart_click", "Click with auto-retry and self-healing selectors", {
151
+ selector: zod_1.z.string().describe("Element to click"),
152
+ maxRetries: zod_1.z.number().optional().default(3).describe("Maximum retry attempts"),
153
+ }, async ({ selector, maxRetries }) => {
154
+ const b = await getBrowser();
155
+ const result = await b.smartClick(selector, { maxRetries });
156
+ return {
157
+ content: [
158
+ {
159
+ type: "text",
160
+ text: JSON.stringify({
161
+ success: result.success,
162
+ attempts: result.attempts.length,
163
+ finalSelector: result.finalSelector,
164
+ message: result.message,
165
+ aiSuggestion: result.aiSuggestion,
166
+ }, null, 2),
167
+ },
168
+ ],
169
+ };
170
+ });
171
+ server.tool("fill", "Fill a form field with text", {
172
+ selector: zod_1.z.string().describe("Input field to fill (name, placeholder, label, or selector)"),
173
+ value: zod_1.z.string().describe("Value to enter"),
174
+ }, async ({ selector, value }) => {
175
+ const b = await getBrowser();
176
+ const result = await b.fill(selector, value);
177
+ return {
178
+ content: [
179
+ {
180
+ type: "text",
181
+ text: JSON.stringify({
182
+ success: result.success,
183
+ message: result.message,
184
+ }, null, 2),
185
+ },
186
+ ],
187
+ };
188
+ });
189
+ // =========================================================================
190
+ // Extraction Tools
191
+ // =========================================================================
192
+ server.tool("screenshot", "Take a screenshot of the current page", {
193
+ path: zod_1.z.string().optional().describe("Optional path to save the screenshot"),
194
+ }, async ({ path }) => {
195
+ const b = await getBrowser();
196
+ const file = await b.screenshot(path);
197
+ return {
198
+ content: [
199
+ {
200
+ type: "text",
201
+ text: JSON.stringify({ screenshot: file }, null, 2),
202
+ },
203
+ ],
204
+ };
205
+ });
206
+ server.tool("extract", "Extract data from the page", {
207
+ what: zod_1.z.enum(["links", "headings", "forms", "images", "text"]).describe("What to extract"),
208
+ }, async ({ what }) => {
209
+ const b = await getBrowser();
210
+ const result = await b.extract(what);
211
+ return {
212
+ content: [
213
+ {
214
+ type: "text",
215
+ text: JSON.stringify(result.data, null, 2),
216
+ },
217
+ ],
218
+ };
219
+ });
220
+ // =========================================================================
221
+ // Assertion Tools
222
+ // =========================================================================
223
+ server.tool("assert", "Assert a condition using natural language", {
224
+ assertion: zod_1.z.string().describe("Natural language assertion like \"page contains 'Welcome'\" or \"title is 'Home'\""),
225
+ }, async ({ assertion }) => {
226
+ const b = await getBrowser();
227
+ const result = await b.assert(assertion);
228
+ return {
229
+ content: [
230
+ {
231
+ type: "text",
232
+ text: JSON.stringify({
233
+ passed: result.passed,
234
+ message: result.message,
235
+ actual: result.actual,
236
+ expected: result.expected,
237
+ }, null, 2),
238
+ },
239
+ ],
240
+ };
241
+ });
242
+ // =========================================================================
243
+ // Analysis Tools
244
+ // =========================================================================
245
+ server.tool("analyze_page", "Analyze page structure for forms, buttons, links", {}, async () => {
246
+ const b = await getBrowser();
247
+ const analysis = await b.analyzePage();
248
+ return {
249
+ content: [
250
+ {
251
+ type: "text",
252
+ text: JSON.stringify({
253
+ title: analysis.title,
254
+ forms: analysis.forms.length,
255
+ buttons: analysis.buttons.length,
256
+ links: analysis.links.length,
257
+ hasLogin: analysis.hasLogin,
258
+ hasSearch: analysis.hasSearch,
259
+ hasNavigation: analysis.hasNavigation,
260
+ }, null, 2),
261
+ },
262
+ ],
263
+ };
264
+ });
265
+ server.tool("generate_tests", "Generate test scenarios for a page", {
266
+ url: zod_1.z.string().url().optional().describe("URL to analyze (uses current page if not provided)"),
267
+ }, async ({ url }) => {
268
+ const b = await getBrowser();
269
+ const result = await b.generateTests(url);
270
+ return {
271
+ content: [
272
+ {
273
+ type: "text",
274
+ text: JSON.stringify({
275
+ testsGenerated: result.tests.length,
276
+ tests: result.tests.map(t => ({
277
+ name: t.name,
278
+ description: t.description,
279
+ steps: t.steps.length,
280
+ })),
281
+ }, null, 2),
282
+ },
283
+ ],
284
+ };
285
+ });
286
+ // =========================================================================
287
+ // Session Tools
288
+ // =========================================================================
289
+ server.tool("save_session", "Save browser session (cookies, storage) for later use", {
290
+ name: zod_1.z.string().describe("Name for the saved session"),
291
+ }, async ({ name }) => {
292
+ const b = await getBrowser();
293
+ await b.saveSession(name);
294
+ return {
295
+ content: [
296
+ {
297
+ type: "text",
298
+ text: JSON.stringify({ success: true, sessionName: name }, null, 2),
299
+ },
300
+ ],
301
+ };
302
+ });
303
+ server.tool("load_session", "Load a previously saved session", {
304
+ name: zod_1.z.string().describe("Name of the session to load"),
305
+ }, async ({ name }) => {
306
+ const b = await getBrowser();
307
+ const loaded = await b.loadSession(name);
308
+ return {
309
+ content: [
310
+ {
311
+ type: "text",
312
+ text: JSON.stringify({ success: loaded, sessionName: name }, null, 2),
313
+ },
314
+ ],
315
+ };
316
+ });
317
+ server.tool("list_sessions", "List all saved sessions", {}, async () => {
318
+ const b = await getBrowser();
319
+ const sessions = await b.listSessions();
320
+ return {
321
+ content: [
322
+ {
323
+ type: "text",
324
+ text: JSON.stringify(sessions, null, 2),
325
+ },
326
+ ],
327
+ };
328
+ });
329
+ // =========================================================================
330
+ // Self-Healing Tools
331
+ // =========================================================================
332
+ server.tool("heal_stats", "Get self-healing selector cache statistics", {}, async () => {
333
+ const b = await getBrowser();
334
+ const stats = b.getSelectorCacheStats();
335
+ return {
336
+ content: [
337
+ {
338
+ type: "text",
339
+ text: JSON.stringify(stats, null, 2),
340
+ },
341
+ ],
342
+ };
343
+ });
344
+ // =========================================================================
345
+ // Visual Testing Tools (v7.0.0+)
346
+ // =========================================================================
347
+ server.tool("visual_baseline", "Capture a visual baseline for a URL", {
348
+ url: zod_1.z.string().url().describe("URL to capture baseline for"),
349
+ name: zod_1.z.string().describe("Name for the baseline"),
350
+ }, async ({ url, name }) => {
351
+ const result = await (0, index_js_1.captureVisualBaseline)(url, name, {});
352
+ return {
353
+ content: [
354
+ {
355
+ type: "text",
356
+ text: JSON.stringify({
357
+ success: true,
358
+ name: result.name,
359
+ url: result.url,
360
+ timestamp: result.timestamp,
361
+ }, null, 2),
362
+ },
363
+ ],
364
+ };
365
+ });
366
+ server.tool("visual_regression", "Run AI visual regression test against a baseline", {
367
+ url: zod_1.z.string().url().describe("URL to test"),
368
+ baselineName: zod_1.z.string().describe("Name of baseline to compare against"),
369
+ }, async ({ url, baselineName }) => {
370
+ const result = await (0, index_js_1.runVisualRegression)(url, baselineName);
371
+ return {
372
+ content: [
373
+ {
374
+ type: "text",
375
+ text: JSON.stringify({
376
+ passed: result.passed,
377
+ similarityScore: result.analysis?.similarityScore,
378
+ summary: result.analysis?.summary,
379
+ changes: result.analysis?.changes?.length || 0,
380
+ }, null, 2),
381
+ },
382
+ ],
383
+ };
384
+ });
385
+ server.tool("cross_browser_test", "Test page rendering across multiple browsers", {
386
+ url: zod_1.z.string().url().describe("URL to test"),
387
+ browsers: zod_1.z.array(zod_1.z.enum(["chromium", "firefox", "webkit"])).optional().describe("Browsers to test"),
388
+ }, async ({ url, browsers }) => {
389
+ const result = await (0, index_js_1.runCrossBrowserTest)(url, { browsers });
390
+ return {
391
+ content: [
392
+ {
393
+ type: "text",
394
+ text: JSON.stringify({
395
+ url: result.url,
396
+ overallStatus: result.overallStatus,
397
+ summary: result.summary,
398
+ screenshotCount: result.screenshots.length,
399
+ comparisonCount: result.comparisons.length,
400
+ }, null, 2),
401
+ },
402
+ ],
403
+ };
404
+ });
405
+ server.tool("cross_browser_diff", "Quick diff of page metrics across browsers", {
406
+ url: zod_1.z.string().url().describe("URL to compare"),
407
+ browsers: zod_1.z.array(zod_1.z.enum(["chromium", "firefox", "webkit"])).optional().describe("Browsers to compare"),
408
+ }, async ({ url, browsers }) => {
409
+ const result = await (0, index_js_1.crossBrowserDiff)(url, browsers);
410
+ return {
411
+ content: [
412
+ {
413
+ type: "text",
414
+ text: JSON.stringify({
415
+ url: result.url,
416
+ browsers: result.browsers,
417
+ differences: result.differences,
418
+ metrics: result.metrics,
419
+ }, null, 2),
420
+ },
421
+ ],
422
+ };
423
+ });
424
+ server.tool("responsive_test", "Test page across different viewport sizes", {
425
+ url: zod_1.z.string().url().describe("URL to test"),
426
+ viewports: zod_1.z.array(zod_1.z.string()).optional().describe("Viewport presets (mobile, tablet, desktop, etc.)"),
427
+ }, async ({ url, viewports }) => {
428
+ const result = await (0, index_js_1.runResponsiveTest)(url, { viewports });
429
+ return {
430
+ content: [
431
+ {
432
+ type: "text",
433
+ text: JSON.stringify({
434
+ url: result.url,
435
+ overallStatus: result.overallStatus,
436
+ summary: result.summary,
437
+ viewportsCount: result.screenshots.length,
438
+ }, null, 2),
439
+ },
440
+ ],
441
+ };
442
+ });
443
+ server.tool("ab_comparison", "Compare two URLs visually (staging vs production)", {
444
+ urlA: zod_1.z.string().url().describe("First URL (e.g., staging)"),
445
+ urlB: zod_1.z.string().url().describe("Second URL (e.g., production)"),
446
+ labelA: zod_1.z.string().optional().describe("Label for first URL"),
447
+ labelB: zod_1.z.string().optional().describe("Label for second URL"),
448
+ }, async ({ urlA, urlB, labelA, labelB }) => {
449
+ const labels = labelA && labelB ? { a: labelA, b: labelB } : undefined;
450
+ const result = await (0, index_js_1.runABComparison)(urlA, urlB, { labels });
451
+ return {
452
+ content: [
453
+ {
454
+ type: "text",
455
+ text: JSON.stringify({
456
+ overallStatus: result.overallStatus,
457
+ similarityScore: result.analysis?.similarityScore,
458
+ summary: result.analysis?.summary,
459
+ changesCount: result.analysis?.changes?.length || 0,
460
+ }, null, 2),
461
+ },
462
+ ],
463
+ };
464
+ });
465
+ // =========================================================================
466
+ // Testing Tools (v6.0.0+)
467
+ // =========================================================================
468
+ server.tool("nl_test_file", "Run natural language test suite from a file", {
469
+ filepath: zod_1.z.string().describe("Path to the test file"),
470
+ }, async ({ filepath }) => {
471
+ const result = await (0, index_js_2.runNLTestFile)(filepath);
472
+ return {
473
+ content: [
474
+ {
475
+ type: "text",
476
+ text: JSON.stringify({
477
+ name: result.name,
478
+ total: result.summary.total,
479
+ passed: result.summary.passed,
480
+ failed: result.summary.failed,
481
+ passRate: `${result.summary.passRate.toFixed(1)}%`,
482
+ duration: result.duration,
483
+ }, null, 2),
484
+ },
485
+ ],
486
+ };
487
+ });
488
+ server.tool("nl_test_inline", "Run natural language tests from inline content", {
489
+ content: zod_1.z.string().describe("Test content with instructions like 'go to https://...' and 'click login'"),
490
+ name: zod_1.z.string().optional().describe("Name for the test suite"),
491
+ }, async ({ content, name }) => {
492
+ const suite = (0, index_js_2.parseNLTestSuite)(content, name || "Inline Test");
493
+ const result = await (0, index_js_2.runNLTestSuite)(suite);
494
+ return {
495
+ content: [
496
+ {
497
+ type: "text",
498
+ text: JSON.stringify({
499
+ name: result.name,
500
+ total: result.summary.total,
501
+ passed: result.summary.passed,
502
+ failed: result.summary.failed,
503
+ passRate: `${result.summary.passRate.toFixed(1)}%`,
504
+ duration: result.duration,
505
+ }, null, 2),
506
+ },
507
+ ],
508
+ };
509
+ });
510
+ server.tool("repair_test", "AI-powered test repair for broken tests", {
511
+ testName: zod_1.z.string().describe("Name for the test"),
512
+ steps: zod_1.z.array(zod_1.z.string()).describe("Test step instructions"),
513
+ autoApply: zod_1.z.boolean().optional().describe("Automatically apply repairs"),
514
+ }, async ({ testName, steps, autoApply }) => {
515
+ const testCase = {
516
+ name: testName,
517
+ steps: steps.map(instruction => ({
518
+ instruction,
519
+ action: "unknown",
520
+ })),
521
+ };
522
+ const result = await (0, index_js_2.repairTest)(testCase, { autoApply: autoApply || false });
523
+ return {
524
+ content: [
525
+ {
526
+ type: "text",
527
+ text: JSON.stringify({
528
+ originalTest: result.originalTest.name,
529
+ failedSteps: result.failedSteps,
530
+ repairedSteps: result.repairedSteps,
531
+ repairedTestPasses: result.repairedTestPasses,
532
+ repairs: result.failureAnalyses.map(a => ({
533
+ step: a.step.instruction,
534
+ error: a.error,
535
+ suggestion: a.suggestions[0]?.suggestedInstruction || "No suggestion",
536
+ })),
537
+ }, null, 2),
538
+ },
539
+ ],
540
+ };
541
+ });
542
+ server.tool("detect_flaky_tests", "Detect flaky/unreliable tests by running multiple times", {
543
+ testContent: zod_1.z.string().describe("Test content to analyze"),
544
+ runs: zod_1.z.number().optional().default(5).describe("Number of times to run each test"),
545
+ threshold: zod_1.z.number().optional().default(20).describe("Flakiness threshold percentage"),
546
+ }, async ({ testContent, runs, threshold }) => {
547
+ const suite = (0, index_js_2.parseNLTestSuite)(testContent, "Flaky Test Analysis");
548
+ const result = await (0, index_js_2.detectFlakyTests)(suite, { runs, flakinessThreshold: threshold });
549
+ return {
550
+ content: [
551
+ {
552
+ type: "text",
553
+ text: JSON.stringify({
554
+ suiteName: result.suiteName,
555
+ totalTests: result.summary.totalTests,
556
+ stablePass: result.summary.stablePassTests,
557
+ stableFail: result.summary.stableFailTests,
558
+ flakyTests: result.summary.flakyTests,
559
+ overallFlakiness: `${result.summary.overallFlakinessScore.toFixed(1)}%`,
560
+ analyses: result.testAnalyses.map(a => ({
561
+ test: a.testName,
562
+ classification: a.classification,
563
+ passRate: `${((a.passCount / a.totalRuns) * 100).toFixed(0)}%`,
564
+ flakiness: `${a.flakinessScore}%`,
565
+ })),
566
+ }, null, 2),
567
+ },
568
+ ],
569
+ };
570
+ });
571
+ server.tool("coverage_map", "Generate test coverage map for a site", {
572
+ baseUrl: zod_1.z.string().url().describe("Base URL to analyze"),
573
+ testFiles: zod_1.z.array(zod_1.z.string()).describe("Array of test file paths"),
574
+ maxPages: zod_1.z.number().optional().default(100).describe("Maximum pages to crawl"),
575
+ }, async ({ baseUrl, testFiles, maxPages }) => {
576
+ const result = await (0, index_js_2.generateCoverageMap)(baseUrl, testFiles, { maxPages });
577
+ return {
578
+ content: [
579
+ {
580
+ type: "text",
581
+ text: JSON.stringify({
582
+ totalPages: result.sitePages.length,
583
+ testedPages: result.testedPages.length,
584
+ untestedPages: result.analysis.untestedPages,
585
+ overallCoverage: `${result.analysis.coveragePercent.toFixed(1)}%`,
586
+ gaps: result.gaps.slice(0, 10).map(g => ({
587
+ url: g.page.url,
588
+ priority: g.priority,
589
+ reason: g.reason,
590
+ })),
591
+ }, null, 2),
592
+ },
593
+ ],
594
+ };
595
+ });
596
+ // =========================================================================
597
+ // Analysis Tools (v4.0.0+)
598
+ // =========================================================================
599
+ server.tool("hunt_bugs", "Autonomous bug hunting - crawl and find issues", {
600
+ url: zod_1.z.string().url().describe("Starting URL to hunt from"),
601
+ maxPages: zod_1.z.number().optional().default(10).describe("Maximum pages to visit"),
602
+ timeout: zod_1.z.number().optional().default(60000).describe("Timeout in milliseconds"),
603
+ }, async ({ url, maxPages, timeout }) => {
604
+ const b = await getBrowser();
605
+ const result = await (0, index_js_3.huntBugs)(b, url, { maxPages, timeout });
606
+ return {
607
+ content: [
608
+ {
609
+ type: "text",
610
+ text: JSON.stringify({
611
+ pagesVisited: result.pagesVisited,
612
+ bugsFound: result.bugs.length,
613
+ duration: result.duration,
614
+ bugs: result.bugs.slice(0, 10),
615
+ }, null, 2),
616
+ },
617
+ ],
618
+ };
619
+ });
620
+ server.tool("chaos_test", "Inject failures and test resilience", {
621
+ url: zod_1.z.string().url().describe("URL to test"),
622
+ networkLatency: zod_1.z.number().optional().describe("Simulate network latency (ms)"),
623
+ offline: zod_1.z.boolean().optional().describe("Simulate offline mode"),
624
+ blockUrls: zod_1.z.array(zod_1.z.string()).optional().describe("URL patterns to block"),
625
+ }, async ({ url, networkLatency, offline, blockUrls }) => {
626
+ const b = await getBrowser();
627
+ const result = await (0, index_js_3.runChaosTest)(b, url, { networkLatency, offline, blockUrls });
628
+ return {
629
+ content: [
630
+ {
631
+ type: "text",
632
+ text: JSON.stringify({
633
+ passed: result.passed,
634
+ errors: result.errors,
635
+ duration: result.duration,
636
+ }, null, 2),
637
+ },
638
+ ],
639
+ };
640
+ });
641
+ server.tool("compare_personas", "Compare how different user personas experience a journey", {
642
+ url: zod_1.z.string().url().describe("Starting URL"),
643
+ goal: zod_1.z.string().describe("Goal to accomplish"),
644
+ personas: zod_1.z.array(zod_1.z.string()).describe("Persona names to compare"),
645
+ }, async ({ url, goal, personas }) => {
646
+ const result = await (0, index_js_3.comparePersonas)({
647
+ startUrl: url,
648
+ goal,
649
+ personas,
650
+ });
651
+ return {
652
+ content: [
653
+ {
654
+ type: "text",
655
+ text: JSON.stringify({
656
+ url: result.url,
657
+ goal: result.goal,
658
+ personasCompared: result.personas.length,
659
+ summary: result.summary,
660
+ }, null, 2),
661
+ },
662
+ ],
663
+ };
664
+ });
665
+ server.tool("find_element_by_intent", "AI-powered semantic element finding", {
666
+ intent: zod_1.z.string().describe("Natural language description like 'the cheapest product' or 'login form'"),
667
+ }, async ({ intent }) => {
668
+ const b = await getBrowser();
669
+ const result = await (0, index_js_3.findElementByIntent)(b, intent);
670
+ return {
671
+ content: [
672
+ {
673
+ type: "text",
674
+ text: JSON.stringify(result || { found: false, message: "No matching element found" }, null, 2),
675
+ },
676
+ ],
677
+ };
678
+ });
679
+ // =========================================================================
680
+ // Performance Tools (v6.4.0+)
681
+ // =========================================================================
682
+ server.tool("perf_baseline", "Capture performance baseline for a URL", {
683
+ url: zod_1.z.string().url().describe("URL to capture baseline for"),
684
+ name: zod_1.z.string().describe("Name for the baseline"),
685
+ runs: zod_1.z.number().optional().default(3).describe("Number of runs to average"),
686
+ }, async ({ url, name, runs }) => {
687
+ const result = await (0, index_js_4.capturePerformanceBaseline)(url, { name, runs });
688
+ return {
689
+ content: [
690
+ {
691
+ type: "text",
692
+ text: JSON.stringify({
693
+ name: result.name,
694
+ url: result.url,
695
+ lcp: result.metrics.lcp,
696
+ fcp: result.metrics.fcp,
697
+ ttfb: result.metrics.ttfb,
698
+ cls: result.metrics.cls,
699
+ }, null, 2),
700
+ },
701
+ ],
702
+ };
703
+ });
704
+ server.tool("perf_regression", "Detect performance regression against baseline", {
705
+ url: zod_1.z.string().url().describe("URL to test"),
706
+ baselineName: zod_1.z.string().describe("Name of baseline to compare against"),
707
+ thresholdLcp: zod_1.z.number().optional().default(20).describe("LCP threshold percentage"),
708
+ }, async ({ url, baselineName, thresholdLcp }) => {
709
+ const result = await (0, index_js_4.detectPerformanceRegression)(url, baselineName, {
710
+ thresholds: { lcp: thresholdLcp },
711
+ });
712
+ return {
713
+ content: [
714
+ {
715
+ type: "text",
716
+ text: JSON.stringify({
717
+ passed: result.passed,
718
+ regressions: result.regressions,
719
+ currentMetrics: result.currentMetrics,
720
+ baseline: result.baseline.name,
721
+ }, null, 2),
722
+ },
723
+ ],
724
+ };
725
+ });
726
+ server.tool("list_baselines", "List all saved baselines (visual and performance)", {}, async () => {
727
+ const visualBaselines = await (0, index_js_1.listVisualBaselines)();
728
+ const perfBaselines = await (0, index_js_4.listPerformanceBaselines)();
729
+ return {
730
+ content: [
731
+ {
732
+ type: "text",
733
+ text: JSON.stringify({
734
+ visual: visualBaselines,
735
+ performance: perfBaselines,
736
+ }, null, 2),
737
+ },
738
+ ],
739
+ };
740
+ });
741
+ }
742
+ /**
743
+ * Create a configured MCP server instance
744
+ */
745
+ function createMcpServer() {
746
+ const server = new mcp_js_1.McpServer({
747
+ name: "cbrowser",
748
+ version: "7.4.2",
749
+ });
750
+ configureMcpTools(server);
751
+ return server;
752
+ }
753
+ /**
754
+ * Handle incoming HTTP MCP request
755
+ */
756
+ async function handleMcpRequest(req, res, transport) {
757
+ // Handle CORS
758
+ res.setHeader("Access-Control-Allow-Origin", "*");
759
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
760
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
761
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
762
+ if (req.method === "OPTIONS") {
763
+ res.writeHead(204);
764
+ res.end();
765
+ return;
766
+ }
767
+ // Parse body for POST requests
768
+ if (req.method === "POST") {
769
+ const chunks = [];
770
+ for await (const chunk of req) {
771
+ chunks.push(chunk);
772
+ }
773
+ const body = Buffer.concat(chunks).toString("utf-8");
774
+ const parsedBody = body ? JSON.parse(body) : undefined;
775
+ await transport.handleRequest(req, res, parsedBody);
776
+ }
777
+ else {
778
+ await transport.handleRequest(req, res);
779
+ }
780
+ }
781
+ /**
782
+ * Start the remote HTTP MCP server
783
+ */
784
+ async function startRemoteMcpServer() {
785
+ const port = parseInt(process.env.PORT || "3000", 10);
786
+ const host = process.env.HOST || "0.0.0.0";
787
+ const sessionMode = process.env.MCP_SESSION_MODE || "stateless";
788
+ const apiKeys = getApiKeys();
789
+ const authEnabled = apiKeys !== null && apiKeys.size > 0;
790
+ console.log(`Starting CBrowser Remote MCP Server v7.4.4...`);
791
+ console.log(`Mode: ${sessionMode}`);
792
+ console.log(`Auth: ${authEnabled ? "enabled" : "disabled (open access)"}`);
793
+ console.log(`Listening on ${host}:${port}`);
794
+ const httpServer = (0, node_http_1.createServer)(async (req, res) => {
795
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
796
+ // Health check endpoint (always open, no auth required)
797
+ if (url.pathname === "/health") {
798
+ res.writeHead(200, { "Content-Type": "application/json" });
799
+ res.end(JSON.stringify({ status: "ok", version: "7.4.4", auth: authEnabled }));
800
+ return;
801
+ }
802
+ // Server info endpoint (always open)
803
+ if (url.pathname === "/info") {
804
+ res.writeHead(200, { "Content-Type": "application/json" });
805
+ res.end(JSON.stringify({
806
+ name: "cbrowser",
807
+ version: "7.4.4",
808
+ description: "Cognitive Browser - AI-powered browser automation with constitutional safety",
809
+ mcp_endpoint: "/mcp",
810
+ auth_required: authEnabled,
811
+ capabilities: ["navigation", "interaction", "visual-testing", "nlp-testing", "performance"],
812
+ }));
813
+ return;
814
+ }
815
+ // Auth check for protected endpoints
816
+ if (authEnabled && apiKeys) {
817
+ if (!validateApiKey(req, apiKeys)) {
818
+ sendUnauthorized(res);
819
+ return;
820
+ }
821
+ }
822
+ // MCP endpoint
823
+ if (url.pathname === "/mcp" || url.pathname === "/") {
824
+ // Get or create session
825
+ const sessionId = req.headers["mcp-session-id"];
826
+ let transport;
827
+ if (sessionMode === "stateful" && sessionId && transports.has(sessionId)) {
828
+ transport = transports.get(sessionId);
829
+ }
830
+ else {
831
+ // Create new transport
832
+ transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
833
+ sessionIdGenerator: sessionMode === "stateful" ? () => (0, node_crypto_1.randomUUID)() : undefined,
834
+ });
835
+ // Create and connect server
836
+ const server = createMcpServer();
837
+ await server.connect(transport);
838
+ // Store transport for stateful mode
839
+ if (sessionMode === "stateful") {
840
+ const newSessionId = transport.sessionId;
841
+ if (newSessionId) {
842
+ transports.set(newSessionId, transport);
843
+ transport.onclose = () => {
844
+ transports.delete(newSessionId);
845
+ };
846
+ }
847
+ }
848
+ }
849
+ await handleMcpRequest(req, res, transport);
850
+ return;
851
+ }
852
+ // 404 for other paths
853
+ res.writeHead(404, { "Content-Type": "application/json" });
854
+ res.end(JSON.stringify({ error: "Not found" }));
855
+ });
856
+ httpServer.listen(port, host, () => {
857
+ console.log(`\nCognitive Browser Remote MCP Server running at http://${host}:${port}`);
858
+ console.log(`\nEndpoints:`);
859
+ console.log(` MCP: http://${host}:${port}/mcp`);
860
+ console.log(` Health: http://${host}:${port}/health`);
861
+ console.log(` Info: http://${host}:${port}/info`);
862
+ if (authEnabled) {
863
+ console.log(`\nAuthentication:`);
864
+ console.log(` Header: Authorization: Bearer <your-api-key>`);
865
+ console.log(` Or: X-API-Key: <your-api-key>`);
866
+ }
867
+ console.log(`\nFor claude.ai custom connector:`);
868
+ console.log(` URL: https://cbrowser-mcp.wyldfyre.ai/mcp`);
869
+ });
870
+ // Graceful shutdown
871
+ const shutdown = async () => {
872
+ console.log("\nShutting down...");
873
+ if (browser) {
874
+ await browser.close();
875
+ }
876
+ httpServer.close();
877
+ process.exit(0);
878
+ };
879
+ process.on("SIGINT", shutdown);
880
+ process.on("SIGTERM", shutdown);
881
+ }
882
+ // Run if executed directly
883
+ if (process.argv[1]?.endsWith("mcp-server-remote.js") ||
884
+ process.argv[1]?.endsWith("mcp-server-remote.ts")) {
885
+ startRemoteMcpServer().catch((err) => {
886
+ console.error("Failed to start remote MCP server:", err);
887
+ process.exit(1);
888
+ });
889
+ }
890
+ //# sourceMappingURL=mcp-server-remote.js.map