playwriter 0.0.33 → 0.0.37

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 (65) hide show
  1. package/dist/aria-snapshot.d.ts +68 -0
  2. package/dist/aria-snapshot.d.ts.map +1 -0
  3. package/dist/aria-snapshot.js +359 -0
  4. package/dist/aria-snapshot.js.map +1 -0
  5. package/dist/cdp-relay.d.ts.map +1 -1
  6. package/dist/cdp-relay.js +95 -5
  7. package/dist/cdp-relay.js.map +1 -1
  8. package/dist/cdp-session.d.ts +24 -3
  9. package/dist/cdp-session.d.ts.map +1 -1
  10. package/dist/cdp-session.js +23 -0
  11. package/dist/cdp-session.js.map +1 -1
  12. package/dist/debugger-api.md +4 -3
  13. package/dist/debugger.d.ts +4 -3
  14. package/dist/debugger.d.ts.map +1 -1
  15. package/dist/debugger.js +3 -1
  16. package/dist/debugger.js.map +1 -1
  17. package/dist/editor-api.md +2 -2
  18. package/dist/editor.d.ts +2 -2
  19. package/dist/editor.d.ts.map +1 -1
  20. package/dist/editor.js +1 -0
  21. package/dist/editor.js.map +1 -1
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/mcp.d.ts.map +1 -1
  27. package/dist/mcp.js +151 -14
  28. package/dist/mcp.js.map +1 -1
  29. package/dist/mcp.test.js +340 -5
  30. package/dist/mcp.test.js.map +1 -1
  31. package/dist/protocol.d.ts +12 -1
  32. package/dist/protocol.d.ts.map +1 -1
  33. package/dist/react-source.d.ts +3 -3
  34. package/dist/react-source.d.ts.map +1 -1
  35. package/dist/react-source.js +3 -1
  36. package/dist/react-source.js.map +1 -1
  37. package/dist/scoped-fs.d.ts +94 -0
  38. package/dist/scoped-fs.d.ts.map +1 -0
  39. package/dist/scoped-fs.js +356 -0
  40. package/dist/scoped-fs.js.map +1 -0
  41. package/dist/styles-api.md +3 -3
  42. package/dist/styles.d.ts +3 -3
  43. package/dist/styles.d.ts.map +1 -1
  44. package/dist/styles.js +3 -1
  45. package/dist/styles.js.map +1 -1
  46. package/package.json +13 -13
  47. package/src/aria-snapshot.ts +446 -0
  48. package/src/assets/aria-labels-github-snapshot.txt +605 -0
  49. package/src/assets/aria-labels-github.png +0 -0
  50. package/src/assets/aria-labels-google-snapshot.txt +110 -0
  51. package/src/assets/aria-labels-google.png +0 -0
  52. package/src/assets/aria-labels-hacker-news-snapshot.txt +1023 -0
  53. package/src/assets/aria-labels-hacker-news.png +0 -0
  54. package/src/cdp-relay.ts +103 -5
  55. package/src/cdp-session.ts +50 -3
  56. package/src/debugger.ts +6 -4
  57. package/src/editor.ts +4 -3
  58. package/src/index.ts +8 -0
  59. package/src/mcp.test.ts +424 -5
  60. package/src/mcp.ts +242 -66
  61. package/src/prompt.md +209 -167
  62. package/src/protocol.ts +14 -1
  63. package/src/react-source.ts +5 -3
  64. package/src/scoped-fs.ts +411 -0
  65. package/src/styles.ts +5 -3
package/src/mcp.ts CHANGED
@@ -15,11 +15,15 @@ import { createPatch } from 'diff'
15
15
  import { getCdpUrl, LOG_FILE_PATH, VERSION, sleep } from './utils.js'
16
16
  import { killPortProcess } from 'kill-port-process'
17
17
  import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
18
- import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
18
+ import { getCDPSessionForPage, CDPSession, ICDPSession } from './cdp-session.js'
19
19
  import { Debugger } from './debugger.js'
20
20
  import { Editor } from './editor.js'
21
21
  import { getStylesForLocator, formatStylesAsText, type StylesResult } from './styles.js'
22
22
  import { getReactSource, type ReactSourceLocation } from './react-source.js'
23
+ import { ScopedFS } from './scoped-fs.js'
24
+ import { showAriaRefLabels, hideAriaRefLabels } from './aria-snapshot.js'
25
+ const __filename = fileURLToPath(import.meta.url)
26
+ const __dirname = path.dirname(__filename)
23
27
 
24
28
  class CodeExecutionTimeoutError extends Error {
25
29
  constructor(timeout: number) {
@@ -77,11 +81,13 @@ interface VMContext {
77
81
  clearAllLogs: () => void
78
82
  waitForPageLoad: (options: WaitForPageLoadOptions) => Promise<WaitForPageLoadResult>
79
83
  getCDPSession: (options: { page: Page }) => Promise<CDPSession>
80
- createDebugger: (options: { cdp: CDPSession }) => Debugger
81
- createEditor: (options: { cdp: CDPSession }) => Editor
84
+ createDebugger: (options: { cdp: ICDPSession }) => Debugger
85
+ createEditor: (options: { cdp: ICDPSession }) => Editor
82
86
  getStylesForLocator: (options: { locator: any }) => Promise<StylesResult>
83
87
  formatStylesAsText: (styles: StylesResult) => string
84
88
  getReactSource: (options: { locator: any }) => Promise<ReactSourceLocation | null>
89
+ showAriaRefLabels: (options: { page: Page; interactiveOnly?: boolean }) => Promise<{ snapshot: string; labelCount: number }>
90
+ hideAriaRefLabels: (options: { page: Page }) => Promise<void>
85
91
  require: NodeRequire
86
92
  import: (specifier: string) => Promise<any>
87
93
  }
@@ -112,6 +118,102 @@ const cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
112
118
  const RELAY_PORT = Number(process.env.PLAYWRITER_PORT) || 19988
113
119
  const NO_TABS_ERROR = `No browser tabs are connected. Please install and enable the Playwriter extension on at least one tab: https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe`
114
120
 
121
+ // Create a scoped fs instance that allows access to cwd, /tmp, and os.tmpdir()
122
+ const scopedFs = new ScopedFS()
123
+
124
+ /**
125
+ * Allowlist of Node.js built-in modules that are safe to use in the sandbox.
126
+ * Dangerous modules like child_process, cluster, worker_threads, vm, net are blocked.
127
+ */
128
+ const ALLOWED_MODULES = new Set([
129
+ // Safe utility modules
130
+ 'path',
131
+ 'node:path',
132
+ 'url',
133
+ 'node:url',
134
+ 'querystring',
135
+ 'node:querystring',
136
+ 'punycode',
137
+ 'node:punycode',
138
+
139
+ // Crypto and encoding
140
+ 'crypto',
141
+ 'node:crypto',
142
+ 'buffer',
143
+ 'node:buffer',
144
+ 'string_decoder',
145
+ 'node:string_decoder',
146
+
147
+ // Utilities
148
+ 'util',
149
+ 'node:util',
150
+ 'assert',
151
+ 'node:assert',
152
+ 'events',
153
+ 'node:events',
154
+ 'timers',
155
+ 'node:timers',
156
+
157
+ // Streams and compression
158
+ 'stream',
159
+ 'node:stream',
160
+ 'zlib',
161
+ 'node:zlib',
162
+
163
+ // HTTP (fetch is already available, these are consistent)
164
+ 'http',
165
+ 'node:http',
166
+ 'https',
167
+ 'node:https',
168
+ 'http2',
169
+ 'node:http2',
170
+
171
+ // System info (read-only, useful for debugging)
172
+ 'os',
173
+ 'node:os',
174
+
175
+ // fs is allowed but returns sandboxed version
176
+ 'fs',
177
+ 'node:fs',
178
+ ])
179
+
180
+ /**
181
+ * Create a sandboxed require function that:
182
+ * 1. Returns scoped fs for 'fs' and 'node:fs'
183
+ * 2. Only allows modules in the ALLOWED_MODULES allowlist
184
+ * 3. Blocks all other modules (child_process, net, vm, third-party packages, etc.)
185
+ */
186
+ function createSandboxedRequire(originalRequire: NodeRequire): NodeRequire {
187
+ const sandboxedRequire = ((id: string) => {
188
+ // Check allowlist first
189
+ if (!ALLOWED_MODULES.has(id)) {
190
+ const error = new Error(
191
+ `Module "${id}" is not allowed in the sandbox. ` +
192
+ `Only safe Node.js built-ins are permitted: ${[...ALLOWED_MODULES].filter((m) => !m.startsWith('node:')).join(', ')}`,
193
+ )
194
+ error.name = 'ModuleNotAllowedError'
195
+ throw error
196
+ }
197
+
198
+ // Return sandboxed fs
199
+ if (id === 'fs' || id === 'node:fs') {
200
+ return scopedFs
201
+ }
202
+
203
+ return originalRequire(id)
204
+ }) as NodeRequire
205
+
206
+ // Copy over require properties
207
+ sandboxedRequire.resolve = originalRequire.resolve
208
+ sandboxedRequire.cache = originalRequire.cache
209
+ sandboxedRequire.extensions = originalRequire.extensions
210
+ sandboxedRequire.main = originalRequire.main
211
+
212
+ return sandboxedRequire
213
+ }
214
+
215
+ const sandboxedRequire = createSandboxedRequire(require)
216
+
115
217
  interface RemoteConfig {
116
218
  host: string
117
219
  port: number
@@ -189,6 +291,15 @@ async function sendLogToRelayServer(level: string, ...args: any[]) {
189
291
  }
190
292
  }
191
293
 
294
+ /**
295
+ * Log to both console.error (for early startup) and relay server log file.
296
+ * Fire-and-forget to avoid blocking.
297
+ */
298
+ function mcpLog(...args: any[]) {
299
+ console.error(...args)
300
+ sendLogToRelayServer('log', ...args)
301
+ }
302
+
192
303
  async function getServerVersion(port: number): Promise<string | null> {
193
304
  try {
194
305
  const response = await fetch(`http://127.0.0.1:${port}/version`, {
@@ -211,6 +322,27 @@ async function killRelayServer(port: number): Promise<void> {
211
322
  } catch {}
212
323
  }
213
324
 
325
+ /**
326
+ * Compare two semver versions. Returns:
327
+ * - negative if v1 < v2
328
+ * - 0 if v1 === v2
329
+ * - positive if v1 > v2
330
+ */
331
+ function compareVersions(v1: string, v2: string): number {
332
+ const parts1 = v1.split('.').map(Number)
333
+ const parts2 = v2.split('.').map(Number)
334
+ const len = Math.max(parts1.length, parts2.length)
335
+
336
+ for (let i = 0; i < len; i++) {
337
+ const p1 = parts1[i] || 0
338
+ const p2 = parts2[i] || 0
339
+ if (p1 !== p2) {
340
+ return p1 - p2
341
+ }
342
+ }
343
+ return 0
344
+ }
345
+
214
346
  async function ensureRelayServer(): Promise<void> {
215
347
  const serverVersion = await getServerVersion(RELAY_PORT)
216
348
 
@@ -218,18 +350,30 @@ async function ensureRelayServer(): Promise<void> {
218
350
  return
219
351
  }
220
352
 
353
+ // Don't restart if server version is higher than MCP version.
354
+ // This prevents older MCPs from killing a newer server.
355
+ if (serverVersion !== null && compareVersions(serverVersion, VERSION) > 0) {
356
+ return
357
+ }
358
+
221
359
  if (serverVersion !== null) {
222
- console.error(`CDP relay server version mismatch (server: ${serverVersion}, mcp: ${VERSION}), restarting...`)
360
+ mcpLog(`CDP relay server version mismatch (server: ${serverVersion}, mcp: ${VERSION}), restarting...`)
223
361
  await killRelayServer(RELAY_PORT)
224
362
  } else {
225
- console.error('CDP relay server not running, starting it...')
363
+ mcpLog('CDP relay server not running, starting it...')
226
364
  }
227
365
 
228
- const scriptPath = require.resolve('../dist/start-relay-server.js')
366
+ const dev = process.env.PLAYWRITER_NODE_ENV === 'development'
367
+ const scriptPath = dev
368
+ ? path.resolve(__dirname, '../src/start-relay-server.ts')
369
+ : require.resolve('../dist/start-relay-server.js')
229
370
 
230
- const serverProcess = spawn(process.execPath, [scriptPath], {
371
+ const serverProcess = spawn(dev ? 'tsx' : process.execPath, [scriptPath], {
231
372
  detached: true,
232
373
  stdio: 'ignore',
374
+ env: {
375
+ ...process.env,
376
+ },
233
377
  })
234
378
 
235
379
  serverProcess.unref()
@@ -238,7 +382,7 @@ async function ensureRelayServer(): Promise<void> {
238
382
  await sleep(500)
239
383
  const newVersion = await getServerVersion(RELAY_PORT)
240
384
  if (newVersion === VERSION) {
241
- console.error('CDP relay server started successfully, waiting for extension to connect...')
385
+ mcpLog('CDP relay server started successfully, waiting for extension to connect...')
242
386
  await sleep(1000)
243
387
  return
244
388
  }
@@ -260,6 +404,12 @@ async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
260
404
  const cdpEndpoint = getCdpUrl(remote || { port: RELAY_PORT })
261
405
  const browser = await chromium.connectOverCDP(cdpEndpoint)
262
406
 
407
+ // Clear connection state when browser disconnects (e.g., extension reconnects, relay server restarts)
408
+ browser.on('disconnected', () => {
409
+ mcpLog('Browser disconnected, clearing connection state')
410
+ clearConnectionState()
411
+ })
412
+
263
413
  const contexts = browser.contexts()
264
414
  const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
265
415
 
@@ -343,7 +493,7 @@ function setupPageConsoleListener(page: Page) {
343
493
  pageLogs.shift()
344
494
  }
345
495
  } catch (e) {
346
- console.error('[MCP] Failed to get console message text:', e)
496
+ mcpLog('[MCP] Failed to get console message text:', e)
347
497
  return
348
498
  }
349
499
  })
@@ -375,7 +525,7 @@ async function resetConnection(): Promise<{ browser: Browser; page: Page; contex
375
525
  try {
376
526
  await state.browser.close()
377
527
  } catch (e) {
378
- console.error('Error closing browser:', e)
528
+ mcpLog('Error closing browser:', e)
379
529
  }
380
530
  }
381
531
 
@@ -393,6 +543,12 @@ async function resetConnection(): Promise<{ browser: Browser; page: Page; contex
393
543
  const cdpEndpoint = getCdpUrl(remote || { port: RELAY_PORT })
394
544
  const browser = await chromium.connectOverCDP(cdpEndpoint)
395
545
 
546
+ // Clear connection state when browser disconnects (e.g., extension reconnects, relay server restarts)
547
+ browser.on('disconnected', () => {
548
+ mcpLog('Browser disconnected, clearing connection state')
549
+ clearConnectionState()
550
+ })
551
+
396
552
  const contexts = browser.contexts()
397
553
  const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
398
554
 
@@ -431,53 +587,68 @@ const promptContent =
431
587
  fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'prompt.md'), 'utf-8') +
432
588
  `\n\nfor debugging internal playwriter errors, check playwriter relay server logs at: ${LOG_FILE_PATH}`
433
589
 
434
- server.resource('debugger-api', 'https://playwriter.dev/resources/debugger-api.md', { mimeType: 'text/plain' }, async () => {
435
- const packageJsonPath = require.resolve('playwriter/package.json')
436
- const packageDir = path.dirname(packageJsonPath)
437
- const content = fs.readFileSync(path.join(packageDir, 'dist', 'debugger-api.md'), 'utf-8')
438
-
439
- return {
440
- contents: [
441
- {
442
- uri: 'https://playwriter.dev/resources/debugger-api.md',
443
- text: content,
444
- mimeType: 'text/plain',
445
- },
446
- ],
447
- }
448
- })
449
-
450
- server.resource('editor-api', 'https://playwriter.dev/resources/editor-api.md', { mimeType: 'text/plain' }, async () => {
451
- const packageJsonPath = require.resolve('playwriter/package.json')
452
- const packageDir = path.dirname(packageJsonPath)
453
- const content = fs.readFileSync(path.join(packageDir, 'dist', 'editor-api.md'), 'utf-8')
454
-
455
- return {
456
- contents: [
457
- {
458
- uri: 'https://playwriter.dev/resources/editor-api.md',
459
- text: content,
460
- mimeType: 'text/plain',
461
- },
462
- ],
463
- }
464
- })
590
+ server.resource(
591
+ 'debugger-api',
592
+ 'https://playwriter.dev/resources/debugger-api.md',
593
+ { mimeType: 'text/plain' },
594
+ async () => {
595
+ const packageJsonPath = require.resolve('playwriter/package.json')
596
+ const packageDir = path.dirname(packageJsonPath)
597
+ const content = fs.readFileSync(path.join(packageDir, 'dist', 'debugger-api.md'), 'utf-8')
598
+
599
+ return {
600
+ contents: [
601
+ {
602
+ uri: 'https://playwriter.dev/resources/debugger-api.md',
603
+ text: content,
604
+ mimeType: 'text/plain',
605
+ },
606
+ ],
607
+ }
608
+ },
609
+ )
465
610
 
466
- server.resource('styles-api', 'https://playwriter.dev/resources/styles-api.md', { mimeType: 'text/plain' }, async () => {
467
- const packageJsonPath = require.resolve('playwriter/package.json')
468
- const packageDir = path.dirname(packageJsonPath)
469
- const content = fs.readFileSync(path.join(packageDir, 'dist', 'styles-api.md'), 'utf-8')
611
+ server.resource(
612
+ 'editor-api',
613
+ 'https://playwriter.dev/resources/editor-api.md',
614
+ { mimeType: 'text/plain' },
615
+ async () => {
616
+ const packageJsonPath = require.resolve('playwriter/package.json')
617
+ const packageDir = path.dirname(packageJsonPath)
618
+ const content = fs.readFileSync(path.join(packageDir, 'dist', 'editor-api.md'), 'utf-8')
619
+
620
+ return {
621
+ contents: [
622
+ {
623
+ uri: 'https://playwriter.dev/resources/editor-api.md',
624
+ text: content,
625
+ mimeType: 'text/plain',
626
+ },
627
+ ],
628
+ }
629
+ },
630
+ )
470
631
 
471
- return {
472
- contents: [
473
- {
474
- uri: 'https://playwriter.dev/resources/styles-api.md',
475
- text: content,
476
- mimeType: 'text/plain',
477
- },
478
- ],
479
- }
480
- })
632
+ server.resource(
633
+ 'styles-api',
634
+ 'https://playwriter.dev/resources/styles-api.md',
635
+ { mimeType: 'text/plain' },
636
+ async () => {
637
+ const packageJsonPath = require.resolve('playwriter/package.json')
638
+ const packageDir = path.dirname(packageJsonPath)
639
+ const content = fs.readFileSync(path.join(packageDir, 'dist', 'styles-api.md'), 'utf-8')
640
+
641
+ return {
642
+ contents: [
643
+ {
644
+ uri: 'https://playwriter.dev/resources/styles-api.md',
645
+ text: content,
646
+ mimeType: 'text/plain',
647
+ },
648
+ ],
649
+ }
650
+ },
651
+ )
481
652
 
482
653
  server.tool(
483
654
  'execute',
@@ -520,7 +691,7 @@ server.tool(
520
691
  const page = await getCurrentPage(timeout)
521
692
  const context = state.context || page.context()
522
693
 
523
- console.error('Executing code:', code)
694
+ mcpLog('Executing code:', code)
524
695
 
525
696
  const customConsole = {
526
697
  log: (...args: any[]) => {
@@ -675,11 +846,11 @@ server.tool(
675
846
  return session
676
847
  }
677
848
 
678
- const createDebugger = (options: { cdp: CDPSession }) => {
849
+ const createDebugger = (options: { cdp: ICDPSession }) => {
679
850
  return new Debugger(options)
680
851
  }
681
852
 
682
- const createEditor = (options: { cdp: CDPSession }) => {
853
+ const createEditor = (options: { cdp: ICDPSession }) => {
683
854
  return new Editor(options)
684
855
  }
685
856
 
@@ -709,6 +880,8 @@ server.tool(
709
880
  getStylesForLocator: getStylesForLocatorFn,
710
881
  formatStylesAsText,
711
882
  getReactSource: getReactSourceFn,
883
+ showAriaRefLabels,
884
+ hideAriaRefLabels,
712
885
  resetPlaywright: async () => {
713
886
  const { page: newPage, context: newContext } = await resetConnection()
714
887
 
@@ -728,8 +901,10 @@ server.tool(
728
901
  getStylesForLocator: getStylesForLocatorFn,
729
902
  formatStylesAsText,
730
903
  getReactSource: getReactSourceFn,
904
+ showAriaRefLabels,
905
+ hideAriaRefLabels,
731
906
  resetPlaywright: vmContextObj.resetPlaywright,
732
- require,
907
+ require: sandboxedRequire,
733
908
  // TODO --experimental-vm-modules is needed to make import work in vm
734
909
  import: vmContextObj.import,
735
910
  ...usefulGlobals,
@@ -738,7 +913,7 @@ server.tool(
738
913
  Object.assign(vmContextObj, resetObj)
739
914
  return { page: newPage, context: newContext }
740
915
  },
741
- require,
916
+ require: sandboxedRequire,
742
917
  import: (specifier: string) => import(specifier),
743
918
  ...usefulGlobals,
744
919
  }
@@ -786,15 +961,16 @@ server.tool(
786
961
  }
787
962
  } catch (error: any) {
788
963
  const errorStack = error.stack || error.message
789
- console.error('Error in execute tool:', errorStack)
790
-
791
- const logsText = formatConsoleLogs(consoleLogs, 'Console output (before error)')
792
-
793
964
  const isTimeoutError = error instanceof CodeExecutionTimeoutError || error.name === 'TimeoutError'
965
+
966
+ // Always log to stderr, but only send non-timeout errors to relay server
967
+ console.error('Error in execute tool:', errorStack)
794
968
  if (!isTimeoutError) {
795
- sendLogToRelayServer('error', '[MCP] Error:', errorStack)
969
+ sendLogToRelayServer('error', 'Error in execute tool:', errorStack)
796
970
  }
797
971
 
972
+ const logsText = formatConsoleLogs(consoleLogs, 'Console output (before error)')
973
+
798
974
  const resetHint = isTimeoutError
799
975
  ? ''
800
976
  : '\n\n[HINT: If this is an internal Playwright error, page/browser closed, or connection issue, call the `reset` tool to reconnect. Do NOT reset for other non-connection non-internal errors.]'
@@ -881,7 +1057,7 @@ export async function startMcp(options: { host?: string; token?: string } = {})
881
1057
  if (!remote) {
882
1058
  await ensureRelayServer()
883
1059
  } else {
884
- console.error(`Using remote CDP relay server: ${remote.host}:${remote.port}`)
1060
+ mcpLog(`Using remote CDP relay server: ${remote.host}:${remote.port}`)
885
1061
  await checkRemoteServer(remote)
886
1062
  }
887
1063
  const transport = new StdioServerTransport()