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.
- package/bin.js +1 -1
- package/dist/browser-config.js +1 -3
- package/dist/browser-config.js.map +1 -1
- package/dist/cdp-types.d.ts +25 -0
- package/dist/cdp-types.d.ts.map +1 -0
- package/dist/cdp-types.js +91 -0
- package/dist/cdp-types.js.map +1 -0
- package/dist/extension/cdp-relay.d.ts +12 -0
- package/dist/extension/cdp-relay.d.ts.map +1 -0
- package/dist/extension/cdp-relay.js +378 -0
- package/dist/extension/cdp-relay.js.map +1 -0
- package/dist/extension/protocol.d.ts +29 -0
- package/dist/extension/protocol.d.ts.map +1 -0
- package/dist/extension/protocol.js +2 -0
- package/dist/extension/protocol.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +1 -1
- package/dist/mcp-client.js.map +1 -1
- package/dist/mcp.js +74 -464
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.js +101 -142
- package/dist/mcp.test.js.map +1 -1
- package/dist/prompt.md +41 -487
- package/dist/resource.md +436 -0
- package/dist/start-relay-server.d.ts +8 -0
- package/dist/start-relay-server.d.ts.map +1 -0
- package/dist/start-relay-server.js +33 -0
- package/dist/start-relay-server.js.map +1 -0
- package/package.json +42 -36
- package/src/browser-config.ts +48 -50
- package/src/cdp-types.ts +124 -0
- package/src/extension/cdp-relay.ts +480 -0
- package/src/extension/protocol.ts +34 -0
- package/src/index.ts +1 -0
- package/src/mcp-client.ts +46 -46
- package/src/mcp.test.ts +109 -165
- package/src/mcp.ts +202 -694
- package/src/prompt.md +41 -487
- package/src/resource.md +436 -0
- package/src/snapshots/hacker-news-initial-accessibility.md +243 -127
- package/src/snapshots/shadcn-ui-accessibility.md +300 -510
- 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 '
|
|
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
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
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
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
27
|
+
const RELAY_PORT = 19988
|
|
57
28
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
const cdpAvailable = await isCDPAvailable()
|
|
48
|
+
const scriptPath = require.resolve('../dist/start-relay-server.js')
|
|
126
49
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
50
|
+
const serverProcess = spawn(process.execPath, [scriptPath], {
|
|
51
|
+
detached: true,
|
|
52
|
+
stdio: 'ignore',
|
|
53
|
+
})
|
|
132
54
|
|
|
133
|
-
|
|
134
|
-
const browser = await chromium.connectOverCDP(
|
|
135
|
-
`http://127.0.0.1:${CDP_PORT}`,
|
|
136
|
-
)
|
|
55
|
+
serverProcess.unref()
|
|
137
56
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
let context: BrowserContext
|
|
57
|
+
// wait for extension to connect
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
141
59
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
} else {
|
|
145
|
-
context = await browser.newContext()
|
|
146
|
-
}
|
|
60
|
+
console.error('CDP relay server started')
|
|
61
|
+
}
|
|
147
62
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
79
|
+
state.browser = browser
|
|
80
|
+
state.page = page
|
|
81
|
+
state.context = context
|
|
82
|
+
state.isConnected = true
|
|
247
83
|
|
|
248
|
-
|
|
84
|
+
return { browser, page }
|
|
249
85
|
}
|
|
250
86
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
version: '1.0.0',
|
|
256
|
-
})
|
|
257
|
-
|
|
87
|
+
async function getCurrentPage() {
|
|
88
|
+
if (state.page) {
|
|
89
|
+
return state.page
|
|
90
|
+
}
|
|
258
91
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
315
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
.
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
+
// Browser cleanup is handled by the async cleanup function
|
|
749
257
|
})
|
|
750
258
|
|
|
751
259
|
main().catch(console.error)
|