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.
- package/README.md +159 -46
- package/dist/analysis/bug-hunter.d.ts +0 -0
- package/dist/analysis/bug-hunter.d.ts.map +0 -0
- package/dist/analysis/bug-hunter.js +0 -0
- package/dist/analysis/bug-hunter.js.map +0 -0
- package/dist/analysis/chaos-testing.d.ts +0 -0
- package/dist/analysis/chaos-testing.d.ts.map +0 -0
- package/dist/analysis/chaos-testing.js +0 -0
- package/dist/analysis/chaos-testing.js.map +0 -0
- package/dist/analysis/index.d.ts +0 -0
- package/dist/analysis/index.d.ts.map +0 -0
- package/dist/analysis/index.js +0 -0
- package/dist/analysis/index.js.map +0 -0
- package/dist/analysis/natural-language.d.ts +0 -0
- package/dist/analysis/natural-language.d.ts.map +0 -0
- package/dist/analysis/natural-language.js +0 -0
- package/dist/analysis/natural-language.js.map +0 -0
- package/dist/analysis/persona-comparison.d.ts +0 -0
- package/dist/analysis/persona-comparison.d.ts.map +0 -0
- package/dist/analysis/persona-comparison.js +0 -0
- package/dist/analysis/persona-comparison.js.map +0 -0
- package/dist/browser.d.ts +0 -0
- package/dist/browser.d.ts.map +0 -0
- package/dist/browser.js +0 -0
- package/dist/browser.js.map +0 -0
- package/dist/cli.d.ts +0 -0
- package/dist/cli.d.ts.map +0 -0
- package/dist/cli.js +17 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -0
- package/dist/config.d.ts.map +0 -0
- package/dist/config.js +0 -0
- package/dist/config.js.map +0 -0
- package/dist/daemon.d.ts +0 -0
- package/dist/daemon.d.ts.map +0 -0
- package/dist/daemon.js +0 -0
- package/dist/daemon.js.map +0 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.d.ts.map +0 -0
- package/dist/index.js +0 -0
- package/dist/index.js.map +0 -0
- package/dist/mcp-server-remote.d.ts +23 -0
- package/dist/mcp-server-remote.d.ts.map +1 -0
- package/dist/mcp-server-remote.js +890 -0
- package/dist/mcp-server-remote.js.map +1 -0
- package/dist/mcp-server.d.ts +0 -0
- package/dist/mcp-server.d.ts.map +0 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp-server.js.map +0 -0
- package/dist/performance/index.d.ts +0 -0
- package/dist/performance/index.d.ts.map +0 -0
- package/dist/performance/index.js +0 -0
- package/dist/performance/index.js.map +0 -0
- package/dist/performance/metrics.d.ts +0 -0
- package/dist/performance/metrics.d.ts.map +0 -0
- package/dist/performance/metrics.js +0 -0
- package/dist/performance/metrics.js.map +0 -0
- package/dist/personas.d.ts +0 -0
- package/dist/personas.d.ts.map +0 -0
- package/dist/personas.js +0 -0
- package/dist/personas.js.map +0 -0
- package/dist/testing/coverage.d.ts +0 -0
- package/dist/testing/coverage.d.ts.map +0 -0
- package/dist/testing/coverage.js +0 -0
- package/dist/testing/coverage.js.map +0 -0
- package/dist/testing/flaky-detection.d.ts +0 -0
- package/dist/testing/flaky-detection.d.ts.map +0 -0
- package/dist/testing/flaky-detection.js +0 -0
- package/dist/testing/flaky-detection.js.map +0 -0
- package/dist/testing/index.d.ts +0 -0
- package/dist/testing/index.d.ts.map +0 -0
- package/dist/testing/index.js +0 -0
- package/dist/testing/index.js.map +0 -0
- package/dist/testing/nl-test-suite.d.ts +0 -0
- package/dist/testing/nl-test-suite.d.ts.map +0 -0
- package/dist/testing/nl-test-suite.js +0 -0
- package/dist/testing/nl-test-suite.js.map +0 -0
- package/dist/testing/test-repair.d.ts +0 -0
- package/dist/testing/test-repair.d.ts.map +0 -0
- package/dist/testing/test-repair.js +0 -0
- package/dist/testing/test-repair.js.map +0 -0
- package/dist/types.d.ts +0 -0
- package/dist/types.d.ts.map +0 -0
- package/dist/types.js +0 -0
- package/dist/types.js.map +0 -0
- package/dist/visual/ab-comparison.d.ts +0 -0
- package/dist/visual/ab-comparison.d.ts.map +1 -1
- package/dist/visual/ab-comparison.js +3 -2
- package/dist/visual/ab-comparison.js.map +1 -1
- package/dist/visual/cross-browser.d.ts +0 -0
- package/dist/visual/cross-browser.d.ts.map +1 -1
- package/dist/visual/cross-browser.js +2 -1
- package/dist/visual/cross-browser.js.map +1 -1
- package/dist/visual/index.d.ts +0 -0
- package/dist/visual/index.d.ts.map +0 -0
- package/dist/visual/index.js +0 -0
- package/dist/visual/index.js.map +0 -0
- package/dist/visual/regression.d.ts +0 -0
- package/dist/visual/regression.d.ts.map +1 -1
- package/dist/visual/regression.js +2 -1
- package/dist/visual/regression.js.map +1 -1
- package/dist/visual/responsive.d.ts +0 -0
- package/dist/visual/responsive.d.ts.map +1 -1
- package/dist/visual/responsive.js +3 -2
- package/dist/visual/responsive.js.map +1 -1
- package/docs/REMOTE-MCP-SERVER.md +520 -0
- 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
|