playwriter 0.0.20 → 0.0.22
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/dist/cdp-session.d.ts.map +1 -1
- package/dist/cdp-session.js +45 -28
- package/dist/cdp-session.js.map +1 -1
- package/dist/mcp.js +43 -20
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.d.ts.map +1 -1
- package/dist/mcp.test.js +49 -3
- package/dist/mcp.test.js.map +1 -1
- package/dist/prompt.md +2 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +3 -1
- package/dist/utils.js.map +1 -1
- package/dist/wait-for-page-load.d.ts.map +1 -1
- package/dist/wait-for-page-load.js +19 -7
- package/dist/wait-for-page-load.js.map +1 -1
- package/package.json +14 -12
- package/src/cdp-session.ts +43 -29
- package/src/mcp.test.ts +57 -3
- package/src/mcp.ts +50 -23
- package/src/prompt.md +2 -0
- package/src/utils.ts +3 -1
- package/src/wait-for-page-load.ts +18 -7
package/src/mcp.ts
CHANGED
|
@@ -15,6 +15,13 @@ import { getCdpUrl, LOG_FILE_PATH } from './utils.js'
|
|
|
15
15
|
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
|
|
16
16
|
import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
|
|
17
17
|
|
|
18
|
+
class CodeExecutionTimeoutError extends Error {
|
|
19
|
+
constructor(timeout: number) {
|
|
20
|
+
super(`Code execution timed out after ${timeout}ms`)
|
|
21
|
+
this.name = 'CodeExecutionTimeoutError'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
const require = createRequire(import.meta.url)
|
|
19
26
|
|
|
20
27
|
const usefulGlobals = {
|
|
@@ -94,8 +101,6 @@ const cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
|
|
|
94
101
|
const RELAY_PORT = 19988
|
|
95
102
|
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`
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
99
104
|
async function setDeviceScaleFactorForMacOS(context: BrowserContext): Promise<void> {
|
|
100
105
|
if (os.platform() !== 'darwin') {
|
|
101
106
|
return
|
|
@@ -113,8 +118,25 @@ async function setDeviceScaleFactorForMacOS(context: BrowserContext): Promise<vo
|
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
async function preserveSystemColorScheme(context: BrowserContext): Promise<void> {
|
|
122
|
+
const options = (context as any)._options
|
|
123
|
+
if (!options) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
options.colorScheme = 'no-override'
|
|
127
|
+
options.reducedMotion = 'no-override'
|
|
128
|
+
options.forcedColors = 'no-override'
|
|
129
|
+
await Promise.all(
|
|
130
|
+
context.pages().map((page) => {
|
|
131
|
+
return page.emulateMedia({ colorScheme: null, reducedMotion: null, forcedColors: null }).catch(() => {})
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
116
136
|
function isRegExp(value: any): value is RegExp {
|
|
117
|
-
return
|
|
137
|
+
return (
|
|
138
|
+
typeof value === 'object' && value !== null && typeof value.test === 'function' && typeof value.exec === 'function'
|
|
139
|
+
)
|
|
118
140
|
}
|
|
119
141
|
|
|
120
142
|
function clearUserState() {
|
|
@@ -209,6 +231,7 @@ async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
|
|
|
209
231
|
// Set up console listener for all existing pages
|
|
210
232
|
context.pages().forEach((p) => setupPageConsoleListener(p))
|
|
211
233
|
|
|
234
|
+
await preserveSystemColorScheme(context)
|
|
212
235
|
await setDeviceScaleFactorForMacOS(context)
|
|
213
236
|
|
|
214
237
|
state.browser = browser
|
|
@@ -246,31 +269,32 @@ function setupPageConsoleListener(page: Page) {
|
|
|
246
269
|
browserLogs.set(targetId, [])
|
|
247
270
|
}
|
|
248
271
|
|
|
249
|
-
// Clear logs on navigation/reload
|
|
250
272
|
page.on('framenavigated', (frame) => {
|
|
251
|
-
// Only clear if it's the main frame navigating (page reload/navigation)
|
|
252
273
|
if (frame === page.mainFrame()) {
|
|
253
274
|
browserLogs.set(targetId, [])
|
|
254
275
|
}
|
|
255
276
|
})
|
|
256
277
|
|
|
257
|
-
// Delete logs when page is closed
|
|
258
278
|
page.on('close', () => {
|
|
259
279
|
browserLogs.delete(targetId)
|
|
260
280
|
})
|
|
261
281
|
|
|
262
282
|
page.on('console', (msg) => {
|
|
263
|
-
|
|
283
|
+
try {
|
|
284
|
+
let logEntry = `[${msg.type()}] ${msg.text()}`
|
|
264
285
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const pageLogs = browserLogs.get(targetId)!
|
|
286
|
+
if (!browserLogs.has(targetId)) {
|
|
287
|
+
browserLogs.set(targetId, [])
|
|
288
|
+
}
|
|
289
|
+
const pageLogs = browserLogs.get(targetId)!
|
|
270
290
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
291
|
+
pageLogs.push(logEntry)
|
|
292
|
+
if (pageLogs.length > MAX_LOGS_PER_PAGE) {
|
|
293
|
+
pageLogs.shift()
|
|
294
|
+
}
|
|
295
|
+
} catch (e) {
|
|
296
|
+
console.error('[MCP] Failed to get console message text:', e)
|
|
297
|
+
return
|
|
274
298
|
}
|
|
275
299
|
})
|
|
276
300
|
}
|
|
@@ -287,8 +311,7 @@ async function getCurrentPage(timeout = 5000) {
|
|
|
287
311
|
|
|
288
312
|
if (pages.length > 0) {
|
|
289
313
|
const page = pages[0]
|
|
290
|
-
page.
|
|
291
|
-
await page.emulateMedia({ colorScheme: null })
|
|
314
|
+
await page.waitForLoadState('load', { timeout }).catch(() => {})
|
|
292
315
|
return page
|
|
293
316
|
}
|
|
294
317
|
}
|
|
@@ -334,6 +357,7 @@ async function resetConnection(): Promise<{ browser: Browser; page: Page; contex
|
|
|
334
357
|
// Set up console listener for all existing pages
|
|
335
358
|
context.pages().forEach((p) => setupPageConsoleListener(p))
|
|
336
359
|
|
|
360
|
+
await preserveSystemColorScheme(context)
|
|
337
361
|
await setDeviceScaleFactorForMacOS(context)
|
|
338
362
|
|
|
339
363
|
state.browser = browser
|
|
@@ -350,7 +374,9 @@ const server = new McpServer({
|
|
|
350
374
|
version: '1.0.0',
|
|
351
375
|
})
|
|
352
376
|
|
|
353
|
-
const promptContent =
|
|
377
|
+
const promptContent =
|
|
378
|
+
fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'prompt.md'), 'utf-8') +
|
|
379
|
+
`\n\nfor debugging internal playwriter errors, check playwriter relay server logs at: ${LOG_FILE_PATH}`
|
|
354
380
|
|
|
355
381
|
server.tool(
|
|
356
382
|
'execute',
|
|
@@ -597,7 +623,7 @@ server.tool(
|
|
|
597
623
|
displayErrors: true,
|
|
598
624
|
}),
|
|
599
625
|
new Promise((_, reject) =>
|
|
600
|
-
setTimeout(() => reject(new
|
|
626
|
+
setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout),
|
|
601
627
|
),
|
|
602
628
|
])
|
|
603
629
|
|
|
@@ -636,7 +662,7 @@ server.tool(
|
|
|
636
662
|
|
|
637
663
|
const logsText = formatConsoleLogs(consoleLogs, 'Console output (before error)')
|
|
638
664
|
|
|
639
|
-
const isTimeoutError = error
|
|
665
|
+
const isTimeoutError = error instanceof CodeExecutionTimeoutError || error.name === 'TimeoutError'
|
|
640
666
|
if (!isTimeoutError) {
|
|
641
667
|
sendLogToRelayServer('error', '[MCP] Error:', errorStack)
|
|
642
668
|
}
|
|
@@ -694,12 +720,13 @@ server.tool(
|
|
|
694
720
|
},
|
|
695
721
|
)
|
|
696
722
|
|
|
697
|
-
// Start the server
|
|
698
723
|
async function main() {
|
|
699
724
|
await ensureRelayServer()
|
|
700
725
|
const transport = new StdioServerTransport()
|
|
701
726
|
await server.connect(transport)
|
|
702
|
-
// console.error('Playwright MCP server running on stdio')
|
|
703
727
|
}
|
|
704
728
|
|
|
705
|
-
main().catch(
|
|
729
|
+
main().catch((error) => {
|
|
730
|
+
console.error('Fatal error starting MCP server:', error)
|
|
731
|
+
process.exit(1)
|
|
732
|
+
})
|
package/src/prompt.md
CHANGED
|
@@ -51,9 +51,11 @@ IMPORTANT! never call bringToFront unless specifically asked by the user. It is
|
|
|
51
51
|
|
|
52
52
|
- only call `page.close()` if the user asks you so or if you previously created this page yourself with `newPage`. do not close user created pages unless asked
|
|
53
53
|
- try to never sleep or run `page.waitForTimeout` unless you have to. there are better ways to wait for an element
|
|
54
|
+
- use `page.waitForLoadState('load')` instead of `page.waitForEvent('load')`. `waitForEvent` waits for a future event and will timeout if the page is already loaded, while `waitForLoadState` resolves immediately if already in that state
|
|
54
55
|
- never close browser or context. NEVER call `browser.close()`
|
|
55
56
|
- NEVER use `page.context().newCDPSession()` or `browser.newCDPSession()` - these do not work through the playwriter relay. If you need to send raw CDP commands, use the `getCDPSession` utility function instead.
|
|
56
57
|
|
|
58
|
+
|
|
57
59
|
## always check the current page state after an action
|
|
58
60
|
|
|
59
61
|
after you click a button or submit a form you ALWAYS have to then check what is the current state of the page. you cannot assume what happened after doing an action. instead run the following code to know what happened after the action:
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import { xdgData } from 'xdg-basedir'
|
|
4
5
|
|
|
@@ -8,7 +9,8 @@ export function getCdpUrl({ port = 19988, host = '127.0.0.1' }: { port?: number;
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function getDataDir(): string {
|
|
11
|
-
|
|
12
|
+
const dataDir = xdgData || path.join(os.homedir(), '.local', 'share')
|
|
13
|
+
return path.join(dataDir, 'playwriter')
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export function ensureDataDir(): string {
|
|
@@ -133,15 +133,26 @@ export async function waitForPageLoad(options: WaitForPageLoadOptions): Promise<
|
|
|
133
133
|
await new Promise((resolve) => setTimeout(resolve, minWait))
|
|
134
134
|
|
|
135
135
|
while (Date.now() - startTime < timeout) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
try {
|
|
137
|
+
const { ready, readyState, pendingRequests } = await checkPageReady()
|
|
138
|
+
lastReadyState = readyState
|
|
139
|
+
lastPendingRequests = pendingRequests
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
if (ready) {
|
|
142
|
+
return {
|
|
143
|
+
success: true,
|
|
144
|
+
readyState,
|
|
145
|
+
pendingRequests: [],
|
|
146
|
+
waitTimeMs: Date.now() - startTime,
|
|
147
|
+
timedOut: false,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.error('[waitForPageLoad] page.evaluate failed:', e)
|
|
141
152
|
return {
|
|
142
|
-
success:
|
|
143
|
-
readyState,
|
|
144
|
-
pendingRequests: [],
|
|
153
|
+
success: false,
|
|
154
|
+
readyState: 'error',
|
|
155
|
+
pendingRequests: ['page.evaluate failed - page may have closed or navigated'],
|
|
145
156
|
waitTimeMs: Date.now() - startTime,
|
|
146
157
|
timedOut: false,
|
|
147
158
|
}
|