playwriter 0.0.2 → 0.0.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 (46) hide show
  1. package/bin.js +1 -1
  2. package/dist/browser-config.js +1 -3
  3. package/dist/browser-config.js.map +1 -1
  4. package/dist/cdp-types.d.ts +25 -0
  5. package/dist/cdp-types.d.ts.map +1 -0
  6. package/dist/cdp-types.js +91 -0
  7. package/dist/cdp-types.js.map +1 -0
  8. package/dist/extension/cdp-relay.d.ts +12 -0
  9. package/dist/extension/cdp-relay.d.ts.map +1 -0
  10. package/dist/extension/cdp-relay.js +378 -0
  11. package/dist/extension/cdp-relay.js.map +1 -0
  12. package/dist/extension/protocol.d.ts +29 -0
  13. package/dist/extension/protocol.d.ts.map +1 -0
  14. package/dist/extension/protocol.js +2 -0
  15. package/dist/extension/protocol.js.map +1 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/mcp-client.d.ts.map +1 -1
  21. package/dist/mcp-client.js +1 -1
  22. package/dist/mcp-client.js.map +1 -1
  23. package/dist/mcp.js +74 -464
  24. package/dist/mcp.js.map +1 -1
  25. package/dist/mcp.test.js +101 -142
  26. package/dist/mcp.test.js.map +1 -1
  27. package/dist/prompt.md +41 -487
  28. package/dist/resource.md +436 -0
  29. package/dist/start-relay-server.d.ts +8 -0
  30. package/dist/start-relay-server.d.ts.map +1 -0
  31. package/dist/start-relay-server.js +33 -0
  32. package/dist/start-relay-server.js.map +1 -0
  33. package/package.json +42 -36
  34. package/src/browser-config.ts +48 -50
  35. package/src/cdp-types.ts +124 -0
  36. package/src/extension/cdp-relay.ts +480 -0
  37. package/src/extension/protocol.ts +34 -0
  38. package/src/index.ts +1 -0
  39. package/src/mcp-client.ts +46 -46
  40. package/src/mcp.test.ts +109 -165
  41. package/src/mcp.ts +202 -694
  42. package/src/prompt.md +41 -487
  43. package/src/resource.md +436 -0
  44. package/src/snapshots/hacker-news-initial-accessibility.md +243 -127
  45. package/src/snapshots/shadcn-ui-accessibility.md +300 -510
  46. package/src/start-relay-server.ts +43 -0
package/src/mcp.ts CHANGED
@@ -1,751 +1,259 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
3
  import { z } from 'zod'
4
- import { Page, Browser, BrowserContext, chromium } from 'patchright-core'
4
+ import { Page, Browser, BrowserContext, chromium } from 'playwright-core'
5
5
  import fs from 'node:fs'
6
6
  import path from 'node:path'
7
- import os from 'node:os'
8
- import { spawn } from 'child_process'
9
- import type { ChildProcess } from 'child_process'
10
- import { getBrowserExecutablePath } from './browser-config.js'
11
-
12
- // Chrome executable finding logic moved to browser-config.ts
13
-
14
- // Store for maintaining state across tool calls
15
- interface ToolState {
16
- isConnected: boolean
17
- page: Page | null
18
- browser: Browser | null
19
- chromeProcess: ChildProcess | null
20
- consoleLogs: Map<Page, ConsoleMessage[]>
21
- networkRequests: Map<Page, NetworkRequest[]>
22
- }
7
+ import { spawn } from 'node:child_process'
8
+ import { createRequire } from 'node:module'
9
+ import vm from 'node:vm'
23
10
 
24
- const state: ToolState = {
25
- isConnected: false,
26
- page: null,
27
- browser: null,
28
- chromeProcess: null,
29
- consoleLogs: new Map(),
30
- networkRequests: new Map(),
31
- }
11
+ const require = createRequire(import.meta.url)
32
12
 
33
- interface ConsoleMessage {
34
- type: string
35
- text: string
36
- timestamp: number
37
- location?: {
38
- url: string
39
- lineNumber: number
40
- columnNumber: number
41
- }
13
+ interface State {
14
+ isConnected: boolean
15
+ page: Page | null
16
+ browser: Browser | null
17
+ context: BrowserContext | null
42
18
  }
43
19
 
44
- interface NetworkRequest {
45
- url: string
46
- method: string
47
- status: number
48
- headers: Record<string, string>
49
- timestamp: number
50
- duration: number
51
- size: number
52
- requestBody?: any
53
- responseBody?: any
20
+ const state: State = {
21
+ isConnected: false,
22
+ page: null,
23
+ browser: null,
24
+ context: null,
54
25
  }
55
26
 
56
- const CDP_PORT = 9922
27
+ const RELAY_PORT = 19988
57
28
 
58
- // Check if CDP is available on the specified port
59
- async function isCDPAvailable(): Promise<boolean> {
60
- try {
61
- const response = await fetch(
62
- `http://127.0.0.1:${CDP_PORT}/json/version`,
63
- )
64
- return response.ok
65
- } catch {
66
- return false
67
- }
29
+ async function isPortTaken(port: number): Promise<boolean> {
30
+ try {
31
+ const response = await fetch(`http://localhost:${port}/`)
32
+ return response.ok
33
+ } catch {
34
+ return false
35
+ }
68
36
  }
69
37
 
70
- // Launch Chrome with CDP enabled
71
- async function launchChromeWithCDP(): Promise<ChildProcess> {
72
- const userDataDir = path.join(os.homedir(), '.playwriter')
73
- if (!fs.existsSync(userDataDir)) {
74
- fs.mkdirSync(userDataDir, { recursive: true })
75
- }
38
+ async function ensureRelayServer(): Promise<void> {
39
+ const portTaken = await isPortTaken(RELAY_PORT)
76
40
 
77
- const executablePath = getBrowserExecutablePath()
78
-
79
- const chromeArgs = [
80
- `--remote-debugging-port=${CDP_PORT}`,
81
- `--user-data-dir=${userDataDir}`,
82
- '--no-first-run',
83
- '--no-default-browser-check',
84
- '--disable-session-crashed-bubble',
85
- '--disable-features=DevToolsDebuggingRestrictions',
86
- '--disable-blink-features=AutomationControlled',
87
- '--no-sandbox',
88
- '--disable-web-security',
89
- '--disable-infobars',
90
- '--disable-translate',
91
- '--disable-features=AutomationControlled', // disables --enable-automation
92
- '--disable-background-timer-throttling',
93
- '--disable-popup-blocking',
94
- '--disable-backgrounding-occluded-windows',
95
- '--disable-renderer-backgrounding',
96
- '--disable-window-activation',
97
- '--disable-focus-on-load',
98
- '--no-startup-window',
99
- '--window-position=0,0',
100
- '--disable-site-isolation-trials',
101
- '--disable-features=IsolateOrigins,site-per-process',
102
- ]
103
-
104
- const chromeProcess = spawn(executablePath, chromeArgs, {
105
- detached: true,
106
- stdio: 'ignore',
107
- })
108
-
109
- // Unref the process so it doesn't keep the parent process alive
110
- chromeProcess.unref()
111
-
112
- // Give Chrome time to start up
113
- await new Promise((resolve) => setTimeout(resolve, 2000))
114
-
115
- return chromeProcess
116
- }
41
+ if (portTaken) {
42
+ console.error('CDP relay server already running')
43
+ return
44
+ }
117
45
 
118
- // Ensure connection to Chrome via CDP
119
- async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
120
- if (state.isConnected && state.browser && state.page) {
121
- return { browser: state.browser, page: state.page }
122
- }
46
+ console.error('Starting CDP relay server...')
123
47
 
124
- // Check if CDP is already available
125
- const cdpAvailable = await isCDPAvailable()
48
+ const scriptPath = require.resolve('../dist/start-relay-server.js')
126
49
 
127
- if (!cdpAvailable) {
128
- // Launch Chrome with CDP
129
- const chromeProcess = await launchChromeWithCDP()
130
- state.chromeProcess = chromeProcess
131
- }
50
+ const serverProcess = spawn(process.execPath, [scriptPath], {
51
+ detached: true,
52
+ stdio: 'ignore',
53
+ })
132
54
 
133
- // Connect to Chrome via CDP
134
- const browser = await chromium.connectOverCDP(
135
- `http://127.0.0.1:${CDP_PORT}`,
136
- )
55
+ serverProcess.unref()
137
56
 
138
- // Get the default context
139
- const contexts = browser.contexts()
140
- let context: BrowserContext
57
+ // wait for extension to connect
58
+ await new Promise((resolve) => setTimeout(resolve, 1000))
141
59
 
142
- if (contexts.length > 0) {
143
- context = contexts[0]
144
- } else {
145
- context = await browser.newContext()
146
- }
60
+ console.error('CDP relay server started')
61
+ }
147
62
 
148
- // Generate user agent and set it on context
149
- const ua = require('user-agents')
150
- const userAgent = new ua({
151
- platform: 'MacIntel',
152
- deviceCategory: 'desktop',
153
- })
154
-
155
- // Get or create page
156
- const pages = context.pages()
157
- let page: Page
158
-
159
- if (pages.length > 0) {
160
- page = pages[0]
161
- // Set user agent on existing page
162
- await page.setExtraHTTPHeaders({
163
- 'User-Agent': userAgent.toString(),
164
- })
165
- } else {
166
- page = await context.newPage()
167
- // Set user agent on new page
168
- await page.setExtraHTTPHeaders({
169
- 'User-Agent': userAgent.toString(),
170
- })
171
- }
63
+ async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
64
+ if (state.isConnected && state.browser && state.page) {
65
+ return { browser: state.browser, page: state.page }
66
+ }
172
67
 
173
- // Set up event listeners if not already set
174
- if (!state.isConnected) {
175
- page.on('console', (msg) => {
176
- // Get or create logs array for this page
177
- let pageLogs = state.consoleLogs.get(page)
178
- if (!pageLogs) {
179
- pageLogs = []
180
- state.consoleLogs.set(page, pageLogs)
181
- }
182
-
183
- // Add new log
184
- pageLogs.push({
185
- type: msg.type(),
186
- text: msg.text(),
187
- timestamp: Date.now(),
188
- location: msg.location(),
189
- })
68
+ await ensureRelayServer()
190
69
 
191
- // Keep only last 1000 logs
192
- if (pageLogs.length > 1000) {
193
- pageLogs.shift()
194
- }
195
- })
70
+ const cdpEndpoint = `ws://localhost:${RELAY_PORT}/cdp/${Date.now()}`
71
+ const browser = await chromium.connectOverCDP(cdpEndpoint)
196
72
 
197
- // Clean up logs and network requests when page is closed
198
- page.on('close', () => {
199
- state.consoleLogs.delete(page)
200
- state.networkRequests.delete(page)
201
- })
73
+ const contexts = browser.contexts()
74
+ const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
202
75
 
203
- page.on('request', (request) => {
204
- const startTime = Date.now()
205
- const entry: Partial<NetworkRequest> = {
206
- url: request.url(),
207
- method: request.method(),
208
- headers: request.headers(),
209
- timestamp: startTime,
210
- }
211
-
212
- request
213
- .response()
214
- .then((response) => {
215
- if (response) {
216
- entry.status = response.status()
217
- entry.duration = Date.now() - startTime
218
- entry.size = response.headers()['content-length']
219
- ? parseInt(response.headers()['content-length'])
220
- : 0
221
-
222
- // Get or create requests array for this page
223
- let pageRequests = state.networkRequests.get(page)
224
- if (!pageRequests) {
225
- pageRequests = []
226
- state.networkRequests.set(page, pageRequests)
227
- }
228
-
229
- // Add new request
230
- pageRequests.push(entry as NetworkRequest)
231
-
232
- // Keep only last 1000 requests
233
- if (pageRequests.length > 1000) {
234
- pageRequests.shift()
235
- }
236
- }
237
- })
238
- .catch(() => {
239
- // Handle response errors silently
240
- })
241
- })
242
- }
76
+ const pages = context.pages()
77
+ const page = pages.length > 0 ? pages[0] : await context.newPage()
243
78
 
244
- state.browser = browser
245
- state.page = page
246
- state.isConnected = true
79
+ state.browser = browser
80
+ state.page = page
81
+ state.context = context
82
+ state.isConnected = true
247
83
 
248
- return { browser, page }
84
+ return { browser, page }
249
85
  }
250
86
 
251
- // Initialize MCP server
252
- const server = new McpServer({
253
- name: 'playwriter',
254
- title: 'Playwright MCP Server',
255
- version: '1.0.0',
256
- })
257
-
87
+ async function getCurrentPage() {
88
+ if (state.page) {
89
+ return state.page
90
+ }
258
91
 
259
- // Tool 1: New Page - Creates a new browser page
260
- server.tool(
261
- 'new_page',
262
- 'Create a new browser page in the shared Chrome instance',
263
- {},
264
- async () => {
265
- try {
266
- const { browser, page } = await ensureConnection()
267
-
268
- // Always create a new page
269
- const context = browser.contexts()[0] || await browser.newContext()
270
- const newPage = await context.newPage()
271
-
272
- // Set user agent on new page
273
- const ua = require('user-agents')
274
- const userAgent = new ua({
275
- platform: 'MacIntel',
276
- deviceCategory: 'desktop',
277
- })
278
- await newPage.setExtraHTTPHeaders({
279
- 'User-Agent': userAgent.toString()
280
- })
281
-
282
- // Update state to use the new page
283
- state.page = newPage
284
-
285
- // Set up event listeners on the new page
286
- newPage.on('console', (msg) => {
287
- // Get or create logs array for this page
288
- let pageLogs = state.consoleLogs.get(newPage)
289
- if (!pageLogs) {
290
- pageLogs = []
291
- state.consoleLogs.set(newPage, pageLogs)
292
- }
293
-
294
- // Add new log
295
- pageLogs.push({
296
- type: msg.type(),
297
- text: msg.text(),
298
- timestamp: Date.now(),
299
- location: msg.location(),
300
- })
301
-
302
- // Keep only last 1000 logs
303
- if (pageLogs.length > 1000) {
304
- pageLogs.shift()
305
- }
306
- })
92
+ if (state.browser) {
93
+ const contexts = state.browser.contexts()
94
+ if (contexts.length > 0) {
95
+ const pages = contexts[0].pages()
307
96
 
308
- // Clean up logs and network requests when page is closed
309
- newPage.on('close', () => {
310
- state.consoleLogs.delete(newPage)
311
- state.networkRequests.delete(newPage)
312
- })
97
+ if (pages.length > 0) {
98
+ const page = pages[0]
99
+ await page.emulateMedia({ colorScheme: null })
100
+ return page
101
+ }
102
+ }
103
+ }
313
104
 
314
- newPage.on('request', (request) => {
315
- const startTime = Date.now()
316
- const entry: Partial<NetworkRequest> = {
317
- url: request.url(),
318
- method: request.method(),
319
- headers: request.headers(),
320
- timestamp: startTime,
321
- }
322
-
323
- request
324
- .response()
325
- .then((response) => {
326
- if (response) {
327
- entry.status = response.status()
328
- entry.duration = Date.now() - startTime
329
- entry.size = response.headers()['content-length']
330
- ? parseInt(response.headers()['content-length'])
331
- : 0
332
-
333
- // Get or create requests array for this page
334
- let pageRequests = state.networkRequests.get(newPage)
335
- if (!pageRequests) {
336
- pageRequests = []
337
- state.networkRequests.set(newPage, pageRequests)
338
- }
339
-
340
- // Add new request
341
- pageRequests.push(entry as NetworkRequest)
342
-
343
- // Keep only last 1000 requests
344
- if (pageRequests.length > 1000) {
345
- pageRequests.shift()
346
- }
347
- }
348
- })
349
- .catch(() => {
350
- // Handle response errors silently
351
- })
352
- })
105
+ throw new Error('No page available')
106
+ }
353
107
 
354
- return {
355
- content: [
356
- {
357
- type: 'text',
358
- text: `Created new page. URL: ${newPage.url()}. Total pages: ${context.pages().length}`,
359
- },
360
- ],
361
- }
362
- } catch (error: any) {
363
- return {
364
- content: [
365
- {
366
- type: 'text',
367
- text: `Failed to create new page: ${error.message}`,
368
- },
369
- ],
370
- isError: true,
371
- }
372
- }
373
- },
374
- )
108
+ const server = new McpServer({
109
+ name: 'playwriter',
110
+ title: 'Playwright MCP Server',
111
+ version: '1.0.0',
112
+ })
375
113
 
376
- // Tool 2: Console Logs
377
- server.tool(
378
- 'console_logs',
379
- 'Retrieve console messages from the page',
380
- {
381
- limit: z
382
- .number()
383
- .default(50)
384
- .describe('Maximum number of messages to return'),
385
- type: z
386
- .enum(['log', 'info', 'warning', 'error', 'debug'])
387
- .optional()
388
- .describe('Filter by message type'),
389
- offset: z.number().default(0).describe('Start from this index'),
390
- },
391
- async ({ limit, type, offset }) => {
392
- try {
393
- const { page } = await ensureConnection() // Ensure we're connected first
394
-
395
- // Get logs for current page
396
- const pageLogs = state.consoleLogs.get(page) || []
397
-
398
- // Filter and paginate logs
399
- let logs = [...pageLogs]
400
- if (type) {
401
- logs = logs.filter((log) => log.type === type)
402
- }
403
-
404
- const paginatedLogs = logs.slice(offset, offset + limit)
405
-
406
- // Format logs to look like Chrome console output
407
- let consoleOutput = ''
408
-
409
- if (paginatedLogs.length === 0) {
410
- consoleOutput = 'No console messages'
411
- } else {
412
- consoleOutput = paginatedLogs
413
- .map((log) => {
414
- const timestamp = new Date(
415
- log.timestamp,
416
- ).toLocaleTimeString()
417
- const location = log.location
418
- ? ` ${log.location.url}:${log.location.lineNumber}:${log.location.columnNumber}`
419
- : ''
420
- return `[${log.type}]: ${log.text}${location}`
421
- })
422
- .join('\n')
423
-
424
- if (logs.length > paginatedLogs.length) {
425
- consoleOutput += `\n\n(Showing ${paginatedLogs.length} of ${logs.length} total messages)`
426
- }
427
- }
428
-
429
- return {
430
- content: [
431
- {
432
- type: 'text',
433
- text: consoleOutput,
434
- },
435
- ],
436
- }
437
- } catch (error: any) {
438
- return {
439
- content: [
440
- {
441
- type: 'text',
442
- text: `Failed to get console logs: ${error.message}`,
443
- },
444
- ],
445
- isError: true,
446
- }
447
- }
448
- },
449
- )
114
+ const promptContent = fs.readFileSync(path.join(path.dirname(new URL(import.meta.url).pathname), 'prompt.md'), 'utf-8')
450
115
 
451
- // Tool 3: Network History
452
116
  server.tool(
453
- 'network_history',
454
- 'Get history of network requests',
455
- {
456
- limit: z
457
- .number()
458
- .default(50)
459
- .describe('Maximum number of requests to return'),
460
- urlPattern: z
461
- .string()
462
- .optional()
463
- .describe('Filter by URL pattern (supports wildcards)'),
464
- method: z
465
- .enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
466
- .optional()
467
- .describe('Filter by HTTP method'),
468
- statusCode: z
469
- .object({
470
- min: z.number().optional(),
471
- max: z.number().optional(),
117
+ 'execute',
118
+ promptContent,
119
+ {
120
+ code: z
121
+ .string()
122
+ .describe(
123
+ 'JavaScript code to execute with page and context in scope. Should be one line, using ; to execute multiple statements. To execute complex actions call execute multiple times. ',
124
+ ),
125
+ timeout: z.number().default(3000).describe('Timeout in milliseconds for code execution (default: 3000ms)'),
126
+ },
127
+ async ({ code, timeout }) => {
128
+ await ensureConnection()
129
+
130
+ const page = await getCurrentPage()
131
+ const context = state.context || page.context()
132
+
133
+ console.error('Executing code:', code)
134
+ try {
135
+ const consoleLogs: Array<{ method: string; args: any[] }> = []
136
+
137
+ const customConsole = {
138
+ log: (...args: any[]) => {
139
+ consoleLogs.push({ method: 'log', args })
140
+ },
141
+ info: (...args: any[]) => {
142
+ consoleLogs.push({ method: 'info', args })
143
+ },
144
+ warn: (...args: any[]) => {
145
+ consoleLogs.push({ method: 'warn', args })
146
+ },
147
+ error: (...args: any[]) => {
148
+ consoleLogs.push({ method: 'error', args })
149
+ },
150
+ debug: (...args: any[]) => {
151
+ consoleLogs.push({ method: 'debug', args })
152
+ },
153
+ }
154
+
155
+ const vmContext = vm.createContext({
156
+ page,
157
+ context,
158
+ state,
159
+ console: customConsole,
160
+ })
161
+
162
+ const wrappedCode = `(async () => { ${code} })()`
163
+
164
+ const result = await Promise.race([
165
+ vm.runInContext(wrappedCode, vmContext, {
166
+ timeout,
167
+ displayErrors: true,
168
+ }),
169
+ new Promise((_, reject) =>
170
+ setTimeout(() => reject(new Error(`Code execution timed out after ${timeout}ms`)), timeout),
171
+ ),
172
+ ])
173
+
174
+ let responseText = ''
175
+
176
+ if (consoleLogs.length > 0) {
177
+ responseText += 'Console output:\n'
178
+ consoleLogs.forEach(({ method, args }) => {
179
+ const formattedArgs = args
180
+ .map((arg) => {
181
+ if (typeof arg === 'object') {
182
+ return JSON.stringify(arg, null, 2)
183
+ }
184
+ return String(arg)
472
185
  })
473
- .optional()
474
- .describe('Filter by status code range'),
475
- includeBody: z
476
- .boolean()
477
- .default(false)
478
- .describe('Include request/response bodies'),
479
- },
480
- async ({ limit, urlPattern, method, statusCode, includeBody }) => {
481
- try {
482
- const { page } = await ensureConnection()
483
-
484
- // Get requests for current page
485
- const pageRequests = state.networkRequests.get(page) || []
486
-
487
- // If includeBody is requested, we need to fetch bodies for existing requests
488
- if (includeBody && pageRequests.length > 0) {
489
- // Note: In a real implementation, you'd store bodies during capture
490
- console.warn('Body capture not implemented in this example')
491
- }
492
-
493
- // Filter requests
494
- let requests = [...pageRequests]
495
-
496
- if (urlPattern) {
497
- const pattern = new RegExp(urlPattern.replace(/\*/g, '.*'))
498
- requests = requests.filter((req) => pattern.test(req.url))
499
- }
500
-
501
- if (method) {
502
- requests = requests.filter((req) => req.method === method)
503
- }
504
-
505
- if (statusCode) {
506
- requests = requests.filter((req) => {
507
- if (statusCode.min && req.status < statusCode.min)
508
- return false
509
- if (statusCode.max && req.status > statusCode.max)
510
- return false
511
- return true
512
- })
513
- }
514
-
515
- const limitedRequests = requests.slice(-limit)
516
-
517
- return {
518
- content: [
519
- {
520
- type: 'text',
521
- text: JSON.stringify(
522
- {
523
- total: requests.length,
524
- requests: limitedRequests,
525
- },
526
- null,
527
- 2,
528
- ),
529
- },
530
- ],
531
- }
532
- } catch (error: any) {
533
- return {
534
- content: [
535
- {
536
- type: 'text',
537
- text: `Failed to get network history: ${error.message}`,
538
- },
539
- ],
540
- isError: true,
541
- }
542
- }
543
- },
544
- )
545
-
546
- // Tool 4: Accessibility Snapshot - Get page accessibility tree as JSON
547
- server.tool(
548
- 'accessibility_snapshot',
549
- 'Get the accessibility snapshot of the current page as JSON',
550
- {},
551
- async ({}) => {
552
- try {
553
- const { page } = await ensureConnection()
554
-
555
- // Check if the method exists
556
- if (typeof (page as any)._snapshotForAI !== 'function') {
557
- // Fall back to regular accessibility snapshot
558
- const snapshot = await page.accessibility.snapshot({
559
- interestingOnly: true,
560
- root: undefined,
561
- })
562
-
563
- return {
564
- content: [
565
- {
566
- type: 'text',
567
- text: JSON.stringify(snapshot, null, 2),
568
- },
569
- ],
570
- }
571
- }
572
-
573
- const snapshot = await (page as any)._snapshotForAI()
574
-
575
- return {
576
- content: [
577
- {
578
- type: 'text',
579
- text: snapshot,
580
- },
581
- ],
582
- }
583
- } catch (error: any) {
584
- console.error('Accessibility snapshot error:', error)
585
- return {
586
- content: [
587
- {
588
- type: 'text',
589
- text: `Failed to get accessibility snapshot: ${error.message}`,
590
- },
591
- ],
592
- isError: true,
593
- }
594
- }
595
- },
596
- )
597
-
598
- // Tool 5: Execute - Run arbitrary JavaScript code with page and context in scope
599
- const promptContent = fs.readFileSync(
600
- path.join(path.dirname(new URL(import.meta.url).pathname), 'prompt.md'),
601
- 'utf-8',
602
- )
603
-
604
- server.tool(
605
- 'execute',
606
- promptContent,
607
- {
608
- code: z
609
- .string()
610
- .describe(
611
- 'JavaScript code to execute with page and context in scope. Should be one line, using ; to execute multiple statements. To execute complex actions call execute multiple times. ',
612
- ),
613
- timeout: z
614
- .number()
615
- .default(3000)
616
- .describe('Timeout in milliseconds for code execution (default: 3000ms)'),
617
- },
618
- async ({ code, timeout }) => {
619
- const { page } = await ensureConnection()
620
- const context = page.context()
621
- console.error('Executing code:', code)
622
- try {
623
- // Collect console logs during execution
624
- const consoleLogs: Array<{ method: string; args: any[] }> = []
625
-
626
- // Create a custom console object that collects logs
627
- const customConsole = {
628
- log: (...args: any[]) => {
629
- consoleLogs.push({ method: 'log', args })
630
- },
631
- info: (...args: any[]) => {
632
- consoleLogs.push({ method: 'info', args })
633
- },
634
- warn: (...args: any[]) => {
635
- consoleLogs.push({ method: 'warn', args })
636
- },
637
- error: (...args: any[]) => {
638
- consoleLogs.push({ method: 'error', args })
639
- },
640
- debug: (...args: any[]) => {
641
- consoleLogs.push({ method: 'debug', args })
642
- },
643
- }
644
-
645
- // Create a function that has page, context, and console in scope
646
- const executeCode = new Function(
647
- 'page',
648
- 'context',
649
- 'console',
650
- `
651
- return (async () => {
652
- ${code}
653
- })();
654
- `,
655
- )
656
-
657
- // Execute the code with page, context, and custom console with timeout
658
- const result = await Promise.race([
659
- executeCode(page, context, customConsole),
660
- new Promise((_, reject) =>
661
- setTimeout(() => reject(new Error(`Code execution timed out after ${timeout}ms`)), timeout)
662
- )
663
- ])
664
-
665
- // Format the response with both console output and return value
666
- let responseText = ''
667
-
668
- // Add console logs if any
669
- if (consoleLogs.length > 0) {
670
- responseText += 'Console output:\n'
671
- consoleLogs.forEach(({ method, args }) => {
672
- const formattedArgs = args
673
- .map((arg) => {
674
- if (typeof arg === 'object') {
675
- return JSON.stringify(arg, null, 2)
676
- }
677
- return String(arg)
678
- })
679
- .join(' ')
680
- responseText += `[${method}] ${formattedArgs}\n`
681
- })
682
- responseText += '\n'
683
- }
684
-
685
- // Add return value if any
686
- if (result !== undefined) {
687
- responseText += 'Return value:\n'
688
- responseText += JSON.stringify(result, null, 2)
689
- } else if (consoleLogs.length === 0) {
690
- responseText += 'Code executed successfully (no output)'
691
- }
692
-
693
- return {
694
- content: [
695
- {
696
- type: 'text',
697
- text: responseText.trim(),
698
- },
699
- ],
700
- }
701
- } catch (error: any) {
702
- return {
703
- content: [
704
- {
705
- type: 'text',
706
- text: `Error executing code: ${error.message}\n${error.stack}`,
707
- },
708
- ],
709
- isError: true,
710
- }
186
+ .join(' ')
187
+ responseText += `[${method}] ${formattedArgs}\n`
188
+ })
189
+ responseText += '\n'
190
+ }
191
+
192
+ if (result !== undefined) {
193
+ responseText += 'Return value:\n'
194
+ if (typeof result === 'string') {
195
+ responseText += result
196
+ } else {
197
+ responseText += JSON.stringify(result, null, 2)
711
198
  }
712
- },
199
+ } else if (consoleLogs.length === 0) {
200
+ responseText += 'Code executed successfully (no output)'
201
+ }
202
+
203
+ const MAX_LENGTH = 1000
204
+ let finalText = responseText.trim()
205
+ if (finalText.length > MAX_LENGTH) {
206
+ finalText = finalText.slice(0, MAX_LENGTH) + `\n\n[Truncated to ${MAX_LENGTH} characters. Better manage your logs or paginate them to read the full logs]`
207
+ }
208
+
209
+ return {
210
+ content: [
211
+ {
212
+ type: 'text',
213
+ text: finalText,
214
+ },
215
+ ],
216
+ }
217
+ } catch (error: any) {
218
+ return {
219
+ content: [
220
+ {
221
+ type: 'text',
222
+ text: `Error executing code: ${error.message}\n${error.stack || ''}`,
223
+ },
224
+ ],
225
+ isError: true,
226
+ }
227
+ }
228
+ },
713
229
  )
714
230
 
715
231
  // Start the server
716
232
  async function main() {
717
- const transport = new StdioServerTransport()
718
- await server.connect(transport)
719
- console.error('Playwright MCP server running on stdio')
233
+ const transport = new StdioServerTransport()
234
+ await server.connect(transport)
235
+ console.error('Playwright MCP server running on stdio')
720
236
  }
721
237
 
722
- // Cleanup function
723
238
  async function cleanup() {
724
- console.error('Shutting down MCP server...')
725
-
726
- if (state.browser) {
727
- try {
728
- // Close the browser connection but not the Chrome process
729
- // Since we're using CDP, closing the browser object just closes
730
- // the connection, not the actual Chrome instance
731
- await state.browser.close()
732
- } catch (e) {
733
- // Ignore errors during browser close
734
- }
735
- }
239
+ console.error('Shutting down MCP server...')
736
240
 
737
- // Don't kill the Chrome process - let it continue running
738
- // The process was started with detached: true and unref()
739
- // so it will persist after this process exits
241
+ if (state.browser) {
242
+ try {
243
+ await state.browser.close()
244
+ } catch (e) {
245
+ // Ignore errors during browser close
246
+ }
247
+ }
740
248
 
741
- process.exit(0)
249
+ process.exit(0)
742
250
  }
743
251
 
744
252
  // Handle process termination
745
253
  process.on('SIGINT', cleanup)
746
254
  process.on('SIGTERM', cleanup)
747
255
  process.on('exit', () => {
748
- // Browser cleanup is handled by the async cleanup function
256
+ // Browser cleanup is handled by the async cleanup function
749
257
  })
750
258
 
751
259
  main().catch(console.error)