e2e-pilot 0.0.69

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 (152) hide show
  1. package/bin.js +3 -0
  2. package/dist/aria-snapshot.d.ts +95 -0
  3. package/dist/aria-snapshot.d.ts.map +1 -0
  4. package/dist/aria-snapshot.js +490 -0
  5. package/dist/aria-snapshot.js.map +1 -0
  6. package/dist/bippy.js +971 -0
  7. package/dist/cdp-relay.d.ts +16 -0
  8. package/dist/cdp-relay.d.ts.map +1 -0
  9. package/dist/cdp-relay.js +715 -0
  10. package/dist/cdp-relay.js.map +1 -0
  11. package/dist/cdp-session.d.ts +42 -0
  12. package/dist/cdp-session.d.ts.map +1 -0
  13. package/dist/cdp-session.js +154 -0
  14. package/dist/cdp-session.js.map +1 -0
  15. package/dist/cdp-types.d.ts +63 -0
  16. package/dist/cdp-types.d.ts.map +1 -0
  17. package/dist/cdp-types.js +91 -0
  18. package/dist/cdp-types.js.map +1 -0
  19. package/dist/cli.d.ts +3 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +213 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/create-logger.d.ts +9 -0
  24. package/dist/create-logger.d.ts.map +1 -0
  25. package/dist/create-logger.js +25 -0
  26. package/dist/create-logger.js.map +1 -0
  27. package/dist/debugger-api.md +458 -0
  28. package/dist/debugger-examples-types.d.ts +24 -0
  29. package/dist/debugger-examples-types.d.ts.map +1 -0
  30. package/dist/debugger-examples-types.js +2 -0
  31. package/dist/debugger-examples-types.js.map +1 -0
  32. package/dist/debugger-examples.d.ts +6 -0
  33. package/dist/debugger-examples.d.ts.map +1 -0
  34. package/dist/debugger-examples.js +53 -0
  35. package/dist/debugger-examples.js.map +1 -0
  36. package/dist/debugger.d.ts +381 -0
  37. package/dist/debugger.d.ts.map +1 -0
  38. package/dist/debugger.js +633 -0
  39. package/dist/debugger.js.map +1 -0
  40. package/dist/editor-api.md +364 -0
  41. package/dist/editor-examples.d.ts +11 -0
  42. package/dist/editor-examples.d.ts.map +1 -0
  43. package/dist/editor-examples.js +124 -0
  44. package/dist/editor-examples.js.map +1 -0
  45. package/dist/editor.d.ts +203 -0
  46. package/dist/editor.d.ts.map +1 -0
  47. package/dist/editor.js +336 -0
  48. package/dist/editor.js.map +1 -0
  49. package/dist/execute.d.ts +50 -0
  50. package/dist/execute.d.ts.map +1 -0
  51. package/dist/execute.js +576 -0
  52. package/dist/execute.js.map +1 -0
  53. package/dist/index.d.ts +11 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +7 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/mcp-client.d.ts +20 -0
  58. package/dist/mcp-client.d.ts.map +1 -0
  59. package/dist/mcp-client.js +56 -0
  60. package/dist/mcp-client.js.map +1 -0
  61. package/dist/mcp.d.ts +5 -0
  62. package/dist/mcp.d.ts.map +1 -0
  63. package/dist/mcp.js +720 -0
  64. package/dist/mcp.js.map +1 -0
  65. package/dist/mcp.test.d.ts +10 -0
  66. package/dist/mcp.test.d.ts.map +1 -0
  67. package/dist/mcp.test.js +2999 -0
  68. package/dist/mcp.test.js.map +1 -0
  69. package/dist/network-capture.d.ts +23 -0
  70. package/dist/network-capture.d.ts.map +1 -0
  71. package/dist/network-capture.js +98 -0
  72. package/dist/network-capture.js.map +1 -0
  73. package/dist/protocol.d.ts +54 -0
  74. package/dist/protocol.d.ts.map +1 -0
  75. package/dist/protocol.js +2 -0
  76. package/dist/protocol.js.map +1 -0
  77. package/dist/react-source.d.ts +13 -0
  78. package/dist/react-source.d.ts.map +1 -0
  79. package/dist/react-source.js +68 -0
  80. package/dist/react-source.js.map +1 -0
  81. package/dist/scoped-fs.d.ts +94 -0
  82. package/dist/scoped-fs.d.ts.map +1 -0
  83. package/dist/scoped-fs.js +356 -0
  84. package/dist/scoped-fs.js.map +1 -0
  85. package/dist/selector-generator.js +8126 -0
  86. package/dist/start-relay-server.d.ts +6 -0
  87. package/dist/start-relay-server.d.ts.map +1 -0
  88. package/dist/start-relay-server.js +33 -0
  89. package/dist/start-relay-server.js.map +1 -0
  90. package/dist/styles-api.md +117 -0
  91. package/dist/styles-examples.d.ts +8 -0
  92. package/dist/styles-examples.d.ts.map +1 -0
  93. package/dist/styles-examples.js +64 -0
  94. package/dist/styles-examples.js.map +1 -0
  95. package/dist/styles.d.ts +27 -0
  96. package/dist/styles.d.ts.map +1 -0
  97. package/dist/styles.js +234 -0
  98. package/dist/styles.js.map +1 -0
  99. package/dist/trace-utils.d.ts +14 -0
  100. package/dist/trace-utils.d.ts.map +1 -0
  101. package/dist/trace-utils.js +21 -0
  102. package/dist/trace-utils.js.map +1 -0
  103. package/dist/utils.d.ts +20 -0
  104. package/dist/utils.d.ts.map +1 -0
  105. package/dist/utils.js +75 -0
  106. package/dist/utils.js.map +1 -0
  107. package/dist/wait-for-page-load.d.ts +16 -0
  108. package/dist/wait-for-page-load.d.ts.map +1 -0
  109. package/dist/wait-for-page-load.js +127 -0
  110. package/dist/wait-for-page-load.js.map +1 -0
  111. package/package.json +67 -0
  112. package/src/aria-snapshot.ts +610 -0
  113. package/src/assets/aria-labels-github-snapshot.txt +605 -0
  114. package/src/assets/aria-labels-github.png +0 -0
  115. package/src/assets/aria-labels-google-snapshot.txt +49 -0
  116. package/src/assets/aria-labels-google.png +0 -0
  117. package/src/assets/aria-labels-hacker-news-snapshot.txt +1023 -0
  118. package/src/assets/aria-labels-hacker-news.png +0 -0
  119. package/src/cdp-relay.ts +925 -0
  120. package/src/cdp-session.ts +203 -0
  121. package/src/cdp-timing.md +128 -0
  122. package/src/cdp-types.ts +155 -0
  123. package/src/cli.ts +250 -0
  124. package/src/create-logger.ts +36 -0
  125. package/src/debugger-examples-types.ts +13 -0
  126. package/src/debugger-examples.ts +66 -0
  127. package/src/debugger.md +453 -0
  128. package/src/debugger.ts +713 -0
  129. package/src/editor-examples.ts +148 -0
  130. package/src/editor.ts +390 -0
  131. package/src/execute.ts +763 -0
  132. package/src/index.ts +10 -0
  133. package/src/mcp-client.ts +78 -0
  134. package/src/mcp.test.ts +3596 -0
  135. package/src/mcp.ts +876 -0
  136. package/src/network-capture.ts +140 -0
  137. package/src/prompt.bak.md +323 -0
  138. package/src/prompt.md +7 -0
  139. package/src/protocol.ts +63 -0
  140. package/src/react-source.ts +94 -0
  141. package/src/resource.md +436 -0
  142. package/src/scoped-fs.ts +411 -0
  143. package/src/snapshots/hacker-news-focused-accessibility.md +202 -0
  144. package/src/snapshots/hacker-news-initial-accessibility.md +11 -0
  145. package/src/snapshots/hacker-news-tabbed-accessibility.md +202 -0
  146. package/src/snapshots/shadcn-ui-accessibility.md +11 -0
  147. package/src/start-relay-server.ts +43 -0
  148. package/src/styles-examples.ts +77 -0
  149. package/src/styles.ts +345 -0
  150. package/src/trace-utils.ts +43 -0
  151. package/src/utils.ts +91 -0
  152. package/src/wait-for-page-load.ts +174 -0
package/dist/mcp.js ADDED
@@ -0,0 +1,720 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { chromium } from 'playwright-core';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { spawn } from 'node:child_process';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { createRequire } from 'node:module';
10
+ import dedent from 'string-dedent';
11
+ import { getCdpUrl, LOG_FILE_PATH, VERSION, sleep, RELAY_PORT, getServerVersion, setDeviceScaleFactorForMacOS, preserveSystemColorScheme, formatAsCommentLines, formatDateTimeForPath, } from './utils.js';
12
+ import { killPortProcess } from 'kill-port-process';
13
+ import { executeCode, savePageSnapshots } from './execute.js';
14
+ import { startTracing, stopTracing } from './trace-utils.js';
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+ const require = createRequire(import.meta.url);
18
+ const state = {
19
+ isConnected: false,
20
+ page: null,
21
+ browser: null,
22
+ context: null,
23
+ };
24
+ const userState = {};
25
+ // Store logs per page targetId
26
+ const browserLogs = new Map();
27
+ const MAX_LOGS_PER_PAGE = 5000;
28
+ // Store last accessibility snapshot per page for diff feature
29
+ const lastSnapshots = new WeakMap();
30
+ // Cache CDP sessions per page
31
+ const cdpSessionCache = new WeakMap();
32
+ // Track active recording with tracing
33
+ // The recording state is persisted to disk so it survives MCP restarts (e.g., Claude Code auto-compact)
34
+ let activeRecording = null;
35
+ // Persisted recording state file path
36
+ const RECORDING_STATE_FILE = '.e2e-pilot/recording-state.json';
37
+ function getRecordingStateFilePath() {
38
+ return path.join(process.cwd(), RECORDING_STATE_FILE);
39
+ }
40
+ function saveRecordingState(recording) {
41
+ const stateFilePath = getRecordingStateFilePath();
42
+ const dir = path.dirname(stateFilePath);
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ const state = {
45
+ path: recording.path,
46
+ tracing: recording.tracing,
47
+ };
48
+ fs.writeFileSync(stateFilePath, JSON.stringify(state, null, 2), 'utf-8');
49
+ }
50
+ function loadRecordingState() {
51
+ const stateFilePath = getRecordingStateFilePath();
52
+ try {
53
+ if (!fs.existsSync(stateFilePath)) {
54
+ return null;
55
+ }
56
+ const content = fs.readFileSync(stateFilePath, 'utf-8');
57
+ const state = JSON.parse(content);
58
+ // Verify the recording file still exists
59
+ if (!fs.existsSync(state.path)) {
60
+ clearRecordingState();
61
+ return null;
62
+ }
63
+ return state;
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ function clearRecordingState() {
70
+ const stateFilePath = getRecordingStateFilePath();
71
+ try {
72
+ if (fs.existsSync(stateFilePath)) {
73
+ fs.unlinkSync(stateFilePath);
74
+ }
75
+ }
76
+ catch {
77
+ // Ignore errors when clearing state
78
+ }
79
+ }
80
+ // Restore recording state from disk on MCP startup
81
+ const persistedState = loadRecordingState();
82
+ if (persistedState) {
83
+ activeRecording = {
84
+ path: persistedState.path,
85
+ tracing: persistedState.tracing,
86
+ };
87
+ }
88
+ const NO_TABS_ERROR = `No browser tabs are connected. Please install and enable the E2E Pilot extension on at least one tab: https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe`;
89
+ function getRemoteConfig() {
90
+ const host = process.env.E2E_PILOT_HOST;
91
+ if (!host) {
92
+ return null;
93
+ }
94
+ return {
95
+ host,
96
+ port: RELAY_PORT,
97
+ token: process.env.E2E_PILOT_TOKEN,
98
+ };
99
+ }
100
+ function clearUserState() {
101
+ Object.keys(userState).forEach((key) => delete userState[key]);
102
+ }
103
+ function clearConnectionState() {
104
+ state.isConnected = false;
105
+ state.browser = null;
106
+ state.page = null;
107
+ state.context = null;
108
+ }
109
+ function getLogServerUrl() {
110
+ const remote = getRemoteConfig();
111
+ if (remote) {
112
+ return `http://${remote.host}:${remote.port}/mcp-log`;
113
+ }
114
+ return `http://127.0.0.1:${RELAY_PORT}/mcp-log`;
115
+ }
116
+ async function sendLogToRelayServer(level, ...args) {
117
+ try {
118
+ await fetch(getLogServerUrl(), {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: JSON.stringify({ level, args }),
122
+ signal: AbortSignal.timeout(1000),
123
+ });
124
+ }
125
+ catch {
126
+ // Silently fail if relay server is not available
127
+ }
128
+ }
129
+ /**
130
+ * Log to both console.error (for early startup) and relay server log file.
131
+ * Fire-and-forget to avoid blocking.
132
+ */
133
+ function mcpLog(...args) {
134
+ console.error(...args);
135
+ sendLogToRelayServer('log', ...args);
136
+ }
137
+ async function killRelayServer(port) {
138
+ try {
139
+ await killPortProcess(port);
140
+ await sleep(500);
141
+ }
142
+ catch { }
143
+ }
144
+ /**
145
+ * Compare two semver versions. Returns:
146
+ * - negative if v1 < v2
147
+ * - 0 if v1 === v2
148
+ * - positive if v1 > v2
149
+ */
150
+ function compareVersions(v1, v2) {
151
+ const parts1 = v1.split('.').map(Number);
152
+ const parts2 = v2.split('.').map(Number);
153
+ const len = Math.max(parts1.length, parts2.length);
154
+ for (let i = 0; i < len; i++) {
155
+ const p1 = parts1[i] || 0;
156
+ const p2 = parts2[i] || 0;
157
+ if (p1 !== p2) {
158
+ return p1 - p2;
159
+ }
160
+ }
161
+ return 0;
162
+ }
163
+ async function ensureRelayServer() {
164
+ const serverVersion = await getServerVersion(RELAY_PORT);
165
+ if (serverVersion === VERSION) {
166
+ return;
167
+ }
168
+ // Don't restart if server version is higher than MCP version.
169
+ // This prevents older MCPs from killing a newer server.
170
+ if (serverVersion !== null && compareVersions(serverVersion, VERSION) > 0) {
171
+ return;
172
+ }
173
+ if (serverVersion !== null) {
174
+ mcpLog(`CDP relay server version mismatch (server: ${serverVersion}, mcp: ${VERSION}), restarting...`);
175
+ await killRelayServer(RELAY_PORT);
176
+ }
177
+ else {
178
+ mcpLog('CDP relay server not running, starting it...');
179
+ }
180
+ const dev = process.env.E2E_PILOT_NODE_ENV === 'development';
181
+ const scriptPath = dev
182
+ ? path.resolve(__dirname, '../src/start-relay-server.ts')
183
+ : require.resolve('../dist/start-relay-server.js');
184
+ const serverProcess = spawn(dev ? 'tsx' : process.execPath, [scriptPath], {
185
+ detached: true,
186
+ stdio: 'ignore',
187
+ env: {
188
+ ...process.env,
189
+ },
190
+ });
191
+ serverProcess.unref();
192
+ for (let i = 0; i < 10; i++) {
193
+ await sleep(500);
194
+ const newVersion = await getServerVersion(RELAY_PORT);
195
+ if (newVersion === VERSION) {
196
+ mcpLog('CDP relay server started successfully, waiting for extension to connect...');
197
+ await sleep(1000);
198
+ return;
199
+ }
200
+ }
201
+ throw new Error(`Failed to start CDP relay server after 5 seconds. Check logs at: ${LOG_FILE_PATH}`);
202
+ }
203
+ async function ensureConnection() {
204
+ if (state.isConnected && state.browser && state.page) {
205
+ return { browser: state.browser, page: state.page };
206
+ }
207
+ const remote = getRemoteConfig();
208
+ if (!remote) {
209
+ await ensureRelayServer();
210
+ }
211
+ const cdpEndpoint = getCdpUrl(remote || { port: RELAY_PORT });
212
+ console.log(`Connecting to CDP endpoint: ${cdpEndpoint}`);
213
+ const browser = await chromium.connectOverCDP(cdpEndpoint);
214
+ console.log(`Connected to CDP endpoint`);
215
+ // Clear connection state when browser disconnects (e.g., extension reconnects, relay server restarts)
216
+ browser.on('disconnected', () => {
217
+ mcpLog('Browser disconnected, clearing connection state');
218
+ clearConnectionState();
219
+ });
220
+ const contexts = browser.contexts();
221
+ const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
222
+ // set default action timeout
223
+ context.setDefaultTimeout(5000);
224
+ // Set up console listener for all future pages
225
+ context.on('page', (page) => {
226
+ setupPageConsoleListener(page);
227
+ });
228
+ const pages = context.pages();
229
+ if (pages.length === 0) {
230
+ throw new Error(NO_TABS_ERROR);
231
+ }
232
+ const page = pages[0];
233
+ // Set up console listener for all existing pages
234
+ context.pages().forEach((p) => setupPageConsoleListener(p));
235
+ // These functions only set context-level options, they do NOT send CDP commands to pages.
236
+ // Sending CDP commands (like Emulation.setEmulatedMedia or setDeviceMetricsOverride) to pages
237
+ // immediately after connectOverCDP causes pages to render white/blank with about:blank URLs,
238
+ // because pages may not be fully initialized yet. Playwright applies these settings lazily.
239
+ await preserveSystemColorScheme(context);
240
+ await setDeviceScaleFactorForMacOS(context);
241
+ state.browser = browser;
242
+ state.page = page;
243
+ state.context = context;
244
+ state.isConnected = true;
245
+ return { browser, page };
246
+ }
247
+ async function getPageTargetId(page) {
248
+ if (!page) {
249
+ throw new Error('Page is null or undefined');
250
+ }
251
+ const guid = page._guid;
252
+ if (guid) {
253
+ return guid;
254
+ }
255
+ throw new Error('Could not get page identifier: _guid not available');
256
+ }
257
+ function setupPageConsoleListener(page) {
258
+ // Get targetId synchronously using _guid
259
+ const targetId = page._guid;
260
+ if (!targetId) {
261
+ // If no _guid, silently fail - this shouldn't happen in normal operation
262
+ return;
263
+ }
264
+ // Initialize logs array for this page
265
+ if (!browserLogs.has(targetId)) {
266
+ browserLogs.set(targetId, []);
267
+ }
268
+ page.on('framenavigated', (frame) => {
269
+ if (frame === page.mainFrame()) {
270
+ browserLogs.set(targetId, []);
271
+ }
272
+ });
273
+ page.on('close', () => {
274
+ browserLogs.delete(targetId);
275
+ });
276
+ page.on('console', (msg) => {
277
+ try {
278
+ let logEntry = `[${msg.type()}] ${msg.text()}`;
279
+ if (!browserLogs.has(targetId)) {
280
+ browserLogs.set(targetId, []);
281
+ }
282
+ const pageLogs = browserLogs.get(targetId);
283
+ pageLogs.push(logEntry);
284
+ if (pageLogs.length > MAX_LOGS_PER_PAGE) {
285
+ pageLogs.shift();
286
+ }
287
+ }
288
+ catch (e) {
289
+ mcpLog('[MCP] Failed to get console message text:', e);
290
+ return;
291
+ }
292
+ });
293
+ }
294
+ async function getCurrentPage(timeout = 5000) {
295
+ if (state.page && !state.page.isClosed()) {
296
+ return state.page;
297
+ }
298
+ // Current page is closed or doesn't exist - find another available page
299
+ if (state.browser) {
300
+ const contexts = state.browser.contexts();
301
+ if (contexts.length > 0) {
302
+ const pages = contexts[0].pages().filter((p) => !p.isClosed());
303
+ if (pages.length > 0) {
304
+ const page = pages[0];
305
+ await page.waitForLoadState('domcontentloaded', { timeout }).catch(() => { });
306
+ // Update state.page to the new default page
307
+ state.page = page;
308
+ return page;
309
+ }
310
+ }
311
+ }
312
+ throw new Error(NO_TABS_ERROR);
313
+ }
314
+ async function resetConnection() {
315
+ if (state.browser) {
316
+ try {
317
+ await state.browser.close();
318
+ }
319
+ catch (e) {
320
+ mcpLog('Error closing browser:', e);
321
+ }
322
+ }
323
+ clearConnectionState();
324
+ clearUserState();
325
+ // DO NOT clear browser logs on reset - logs should persist across reconnections
326
+ // browserLogs.clear()
327
+ const remote = getRemoteConfig();
328
+ if (!remote) {
329
+ await ensureRelayServer();
330
+ }
331
+ const cdpEndpoint = getCdpUrl(remote || { port: RELAY_PORT });
332
+ const browser = await chromium.connectOverCDP(cdpEndpoint);
333
+ // Clear connection state when browser disconnects (e.g., extension reconnects, relay server restarts)
334
+ browser.on('disconnected', () => {
335
+ mcpLog('Browser disconnected, clearing connection state');
336
+ clearConnectionState();
337
+ });
338
+ const contexts = browser.contexts();
339
+ const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
340
+ context.setDefaultTimeout(5000);
341
+ // Set up console listener for all future pages
342
+ context.on('page', (page) => {
343
+ setupPageConsoleListener(page);
344
+ });
345
+ const pages = context.pages();
346
+ if (pages.length === 0) {
347
+ throw new Error(NO_TABS_ERROR);
348
+ }
349
+ const page = pages[0];
350
+ // Set up console listener for all existing pages
351
+ context.pages().forEach((p) => setupPageConsoleListener(p));
352
+ await preserveSystemColorScheme(context);
353
+ await setDeviceScaleFactorForMacOS(context);
354
+ state.browser = browser;
355
+ state.page = page;
356
+ state.context = context;
357
+ state.isConnected = true;
358
+ return { browser, page, context };
359
+ }
360
+ const server = new McpServer({
361
+ name: 'e2e-pilot',
362
+ title: 'The better playwright MCP: works as a browser extension. No context bloat. More capable.',
363
+ version: '1.0.0',
364
+ });
365
+ const promptContent = fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'prompt.md'), 'utf-8') +
366
+ `\n\nfor debugging internal errors, check relay server logs at: ${LOG_FILE_PATH}`;
367
+ server.tool('execute', promptContent, {
368
+ intend: z.string().describe('The intent of the call, e.g. "Fill form and submit"'),
369
+ code: z
370
+ .string()
371
+ .optional()
372
+ .describe('js playwright code, has {page, state, context} in scope. Should be one line, using ; to execute multiple statements. you MUST call execute multiple times instead of writing complex scripts in a single tool call.'),
373
+ file: z
374
+ .string()
375
+ .optional()
376
+ .describe('Path to a file containing the code to execute. If provided, code parameter is ignored.'),
377
+ startLine: z
378
+ .number()
379
+ .optional()
380
+ .describe('Start line number (1-indexed) when loading from file. If omitted, starts from beginning.'),
381
+ endLine: z
382
+ .number()
383
+ .optional()
384
+ .describe('End line number (1-indexed, inclusive) when loading from file. If omitted, reads to end of file.'),
385
+ timeout: z.number().default(5000).describe('Timeout in milliseconds for code execution (default: 5000ms)'),
386
+ }, async ({ code, file, startLine, endLine, timeout, intend }) => {
387
+ // Load code from file if file parameter is provided
388
+ let finalCode;
389
+ if (file) {
390
+ if (!fs.existsSync(file)) {
391
+ return {
392
+ content: [{ type: 'text', text: `File not found: ${file}` }],
393
+ isError: true,
394
+ };
395
+ }
396
+ const fileContent = fs.readFileSync(file, 'utf-8');
397
+ const lines = fileContent.split('\n');
398
+ const start = startLine ? startLine - 1 : 0; // Convert to 0-indexed
399
+ const end = endLine ? endLine : lines.length; // endLine is inclusive, so no -1
400
+ finalCode = lines.slice(start, end).join('\n');
401
+ }
402
+ else if (code) {
403
+ finalCode = code;
404
+ }
405
+ else {
406
+ return {
407
+ content: [{ type: 'text', text: 'Either "code" or "file" parameter must be provided' }],
408
+ isError: true,
409
+ };
410
+ }
411
+ const remote = getRemoteConfig();
412
+ if (!remote) {
413
+ await ensureRelayServer();
414
+ }
415
+ await ensureConnection();
416
+ const page = await getCurrentPage(timeout);
417
+ const context = state.context || page.context();
418
+ return executeCode({
419
+ code: finalCode,
420
+ timeout,
421
+ deps: {
422
+ page,
423
+ context,
424
+ userState,
425
+ browserLogs,
426
+ lastSnapshots,
427
+ cdpSessionCache,
428
+ getPageTargetId,
429
+ getCdpUrl: () => getCdpUrl(remote || { port: RELAY_PORT }),
430
+ resetConnection,
431
+ logger: mcpLog,
432
+ sendLogToRelayServer,
433
+ intend,
434
+ activeRecordingPath: activeRecording?.path,
435
+ },
436
+ });
437
+ });
438
+ server.tool('snapshot', dedent `
439
+ Take a snapshot of the current page.
440
+ Returns the path to the snapshot file.
441
+ `, {}, async () => {
442
+ const remote = getRemoteConfig();
443
+ if (!remote) {
444
+ await ensureRelayServer();
445
+ }
446
+ await ensureConnection();
447
+ const page = await getCurrentPage();
448
+ try {
449
+ const snapshotPaths = await savePageSnapshots({ page, lastSnapshots, timeout: 5000 });
450
+ if (!snapshotPaths) {
451
+ return {
452
+ content: [
453
+ {
454
+ type: 'text',
455
+ text: `Failed to save snapshot: _snapshotForAI returned null. The page may not support accessibility snapshots or the page is not fully loaded. Try using 'execute' tool with 'await page.waitForLoadState("domcontentloaded")' first.`,
456
+ },
457
+ ],
458
+ isError: true,
459
+ };
460
+ }
461
+ const viewportSize = page.viewportSize();
462
+ let responseText = `Snapshots saved:\n- Full: ${snapshotPaths.fullPath}`;
463
+ if (viewportSize) {
464
+ responseText += `\n- Viewport: ${viewportSize.width}x${viewportSize.height}`;
465
+ }
466
+ return {
467
+ content: [
468
+ {
469
+ type: 'text',
470
+ text: responseText,
471
+ },
472
+ ],
473
+ };
474
+ }
475
+ catch (error) {
476
+ return {
477
+ content: [
478
+ {
479
+ type: 'text',
480
+ text: `Failed to save snapshot: ${error.message}`,
481
+ },
482
+ ],
483
+ isError: true,
484
+ };
485
+ }
486
+ });
487
+ server.tool('start_recording', dedent `
488
+ Start recording browser interactions to a .js file for e2e test code generation.
489
+ Also starts Playwright tracing to capture screenshots, DOM snapshots, and action logs.
490
+ Creates files at:
491
+ - .e2e-pilot/recordings/YYYY-MM-DD_HH-MM-SS.js (code recording)
492
+ - .e2e-pilot/traces/YYYY-MM-DD_HH-MM-SS.zip (trace file)
493
+ All subsequent execute calls will append their code to this file until stop_recording is called.
494
+ Use stop_recording to stop and get the recording and trace file paths.
495
+ `, {
496
+ title: z.string().describe('A title describing the recording purpose, e.g. "Login flow" or "Create new loan"'),
497
+ pageDescription: z.string().describe('A multiline description of the page state.'),
498
+ }, async ({ title, pageDescription }) => {
499
+ const remote = getRemoteConfig();
500
+ if (!remote) {
501
+ await ensureRelayServer();
502
+ }
503
+ await ensureConnection();
504
+ const context = state.context;
505
+ if (!context) {
506
+ return {
507
+ content: [{ type: 'text', text: 'No browser context available. Call execute first.' }],
508
+ isError: true,
509
+ };
510
+ }
511
+ if (activeRecording) {
512
+ return {
513
+ content: [{ type: 'text', text: `Recording already active at: ${activeRecording.path}` }],
514
+ isError: true,
515
+ };
516
+ }
517
+ const timeout = 5000;
518
+ const page = await getCurrentPage(timeout);
519
+ const pageUrl = page.url();
520
+ const pageTitle = await page.title();
521
+ const now = new Date();
522
+ const dateTimeStr = formatDateTimeForPath(now);
523
+ const recordingDir = path.join(process.cwd(), '.e2e-pilot', 'recordings');
524
+ fs.mkdirSync(recordingDir, { recursive: true });
525
+ const recordingPath = path.join(recordingDir, `${dateTimeStr}.js`);
526
+ const snapshotPaths = await savePageSnapshots({ page, lastSnapshots, timeout });
527
+ // Start tracing
528
+ const tracesBaseDir = path.join(process.cwd(), '.e2e-pilot', 'traces');
529
+ const tracing = await startTracing({ context, baseDir: tracesBaseDir });
530
+ const metaComment = dedent `
531
+ // ${title}
532
+
533
+ // Initial state:
534
+ // Snapshot: ${snapshotPaths?.fullPath}
535
+ // URL: ${pageUrl}
536
+ // Title: ${pageTitle}
537
+ ${formatAsCommentLines(pageDescription)}
538
+
539
+
540
+ `;
541
+ fs.writeFileSync(recordingPath, metaComment, 'utf-8');
542
+ activeRecording = { path: recordingPath, tracing };
543
+ saveRecordingState(activeRecording);
544
+ let responseText = `Recording started: ${recordingPath}\nTracing started: ${tracing.path}\n\nPage URL: ${pageUrl}\nPage Title: ${pageTitle}`;
545
+ if (snapshotPaths) {
546
+ responseText += `\n\nSnapshots saved:\n- Full: ${snapshotPaths.fullPath}`;
547
+ }
548
+ return {
549
+ content: [
550
+ {
551
+ type: 'text',
552
+ text: responseText,
553
+ },
554
+ ],
555
+ };
556
+ });
557
+ server.tool('read_recording', dedent `
558
+ Read the currently active recording file.
559
+ Returns the content of the recording started by start_recording.
560
+ `, {}, async () => {
561
+ if (!activeRecording) {
562
+ return {
563
+ content: [
564
+ {
565
+ type: 'text',
566
+ text: 'No active recording. Call start_recording first.',
567
+ },
568
+ ],
569
+ isError: true,
570
+ };
571
+ }
572
+ try {
573
+ const content = fs.readFileSync(activeRecording.path, 'utf-8');
574
+ return {
575
+ content: [
576
+ {
577
+ type: 'text',
578
+ text: `Recording file: ${activeRecording.path}\n\n${content || '(empty)'}`,
579
+ },
580
+ ],
581
+ };
582
+ }
583
+ catch (error) {
584
+ return {
585
+ content: [
586
+ {
587
+ type: 'text',
588
+ text: `Failed to read recording: ${error.message}`,
589
+ },
590
+ ],
591
+ isError: true,
592
+ };
593
+ }
594
+ });
595
+ server.tool('stop_recording', dedent `
596
+ Stop the active recording and tracing session.
597
+ Returns the paths to the recording file and trace file.
598
+ View traces with: npx playwright show-trace <trace-file>
599
+ `, {}, async () => {
600
+ if (!activeRecording) {
601
+ return {
602
+ content: [{ type: 'text', text: 'No active recording. Call start_recording first.' }],
603
+ isError: true,
604
+ };
605
+ }
606
+ const context = state.context;
607
+ if (!context) {
608
+ const recordingPath = activeRecording.path;
609
+ activeRecording = null;
610
+ clearRecordingState();
611
+ return {
612
+ content: [
613
+ {
614
+ type: 'text',
615
+ text: `Recording stopped: ${recordingPath}\n\nWarning: Could not stop tracing (no browser context).`,
616
+ },
617
+ ],
618
+ };
619
+ }
620
+ const recordingPath = activeRecording.path;
621
+ let tracePath = null;
622
+ let tracingWarning = '';
623
+ try {
624
+ tracePath = await stopTracing({ context, activeTracing: activeRecording.tracing });
625
+ }
626
+ catch (error) {
627
+ // Tracing may fail if MCP was restarted (tracing session lost)
628
+ // Still allow stopping the recording gracefully
629
+ tracingWarning = `\n\nWarning: Could not save trace (${error.message}). This can happen if the MCP was restarted during recording.`;
630
+ }
631
+ activeRecording = null;
632
+ clearRecordingState();
633
+ if (tracePath) {
634
+ return {
635
+ content: [
636
+ {
637
+ type: 'text',
638
+ text: `Recording stopped: ${recordingPath}\nTrace saved: ${tracePath}\n\nView trace with: npx playwright show-trace ${tracePath}`,
639
+ },
640
+ ],
641
+ };
642
+ }
643
+ return {
644
+ content: [
645
+ {
646
+ type: 'text',
647
+ text: `Recording stopped: ${recordingPath}${tracingWarning}`,
648
+ },
649
+ ],
650
+ };
651
+ });
652
+ server.tool('reset', dedent `
653
+ Recreates the CDP connection and resets the browser/page/context. Use this when the MCP stops responding, you get connection errors, if there are no pages in context, assertion failures, page closed, or other issues.
654
+
655
+ After calling this tool, the page and context variables are automatically updated in the execution environment.
656
+
657
+ This tools also removes any custom properties you may have added to the global scope AND clearing all keys from the \`state\` object. Only \`page\`, \`context\`, \`state\` (empty), \`console\`, and utility functions will remain.
658
+
659
+ if playwright always returns all pages as about:blank urls and evaluate does not work you should ask the user to restart Chrome. This is a known Chrome bug.
660
+ `, {}, async () => {
661
+ try {
662
+ const { page, context } = await resetConnection();
663
+ const pagesCount = context.pages().length;
664
+ return {
665
+ content: [
666
+ {
667
+ type: 'text',
668
+ text: `Connection reset successfully. ${pagesCount} page(s) available. Current page URL: ${page.url()}`,
669
+ },
670
+ ],
671
+ };
672
+ }
673
+ catch (error) {
674
+ return {
675
+ content: [
676
+ {
677
+ type: 'text',
678
+ text: `Failed to reset connection: ${error.message}`,
679
+ },
680
+ ],
681
+ isError: true,
682
+ };
683
+ }
684
+ });
685
+ async function checkRemoteServer({ host, port }) {
686
+ const versionUrl = `http://${host}:${port}/version`;
687
+ try {
688
+ const response = await fetch(versionUrl, { signal: AbortSignal.timeout(3000) });
689
+ if (!response.ok) {
690
+ throw new Error(`Server responded with status ${response.status}`);
691
+ }
692
+ }
693
+ catch (error) {
694
+ const isConnectionError = error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError';
695
+ if (isConnectionError) {
696
+ throw new Error(`Cannot connect to remote relay server at ${host}:${port}. ` +
697
+ `Make sure 'npx -y e2e-pilot serve' is running on the host machine.`);
698
+ }
699
+ throw new Error(`Failed to connect to remote relay server: ${error.message}`);
700
+ }
701
+ }
702
+ export async function startMcp(options = {}) {
703
+ if (options.host) {
704
+ process.env.E2E_PILOT_HOST = options.host;
705
+ }
706
+ if (options.token) {
707
+ process.env.E2E_PILOT_TOKEN = options.token;
708
+ }
709
+ const remote = getRemoteConfig();
710
+ if (!remote) {
711
+ await ensureRelayServer();
712
+ }
713
+ else {
714
+ mcpLog(`Using remote CDP relay server: ${remote.host}:${remote.port}`);
715
+ await checkRemoteServer(remote);
716
+ }
717
+ const transport = new StdioServerTransport();
718
+ await server.connect(transport);
719
+ }
720
+ //# sourceMappingURL=mcp.js.map