playwriter 0.0.3 → 0.0.5
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 +384 -0
- package/dist/extension/cdp-relay.js.map +1 -0
- package/dist/extension/protocol.d.ts +35 -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 +93 -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 +45 -491
- 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 +487 -0
- package/src/extension/protocol.ts +41 -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 +223 -693
- package/src/prompt.md +45 -491
- 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/dist/mcp-client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-client.js","sourceRoot":"","sources":["../src/mcp-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAGhF,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,GAAG,MAAM,UAAU,CAAA;AAE1B,MAAM,UAAU,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAMrD,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAc;
|
|
1
|
+
{"version":3,"file":"mcp-client.js","sourceRoot":"","sources":["../src/mcp-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAGhF,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,GAAG,MAAM,UAAU,CAAA;AAE1B,MAAM,UAAU,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAMrD,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAc;IAIlD,MAAM,SAAS,GAAG,IAAI,oBAAoB,CAAC;QACzC,OAAO,EAAE,MAAM;QACf,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,EAAE,GAAG,IAAI,CAAC;QAC3E,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC;QAC9C,MAAM,EAAE,MAAM;QACd,GAAG,EAAE;YACH,GAAG,OAAO,CAAC,GAAG;YACd,KAAK,EAAE,qBAAqB;YAC5B,YAAY,EAAE,GAAG;YACjB,eAAe,EAAE,GAAG;SACrB;KACF,CAAC,CAAA;IAEF,OAAO;QACL,SAAS;QACT,MAAM,EAAE,SAAS,CAAC,MAAO;KAC1B,CAAA;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAgC;IAKpE,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC;QACxB,IAAI,EAAE,OAAO,EAAE,UAAU,IAAI,MAAM;QACnC,OAAO,EAAE,OAAO;KACjB,CAAC,CAAA;IAEF,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,eAAe,CAAC,EAAE,CAAC,CAAA;IAEvD,IAAI,YAAY,GAAG,EAAE,CAAA;IACrB,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAE1B,YAAY,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAA;IACjC,CAAC,CAAC,CAAA;IAEF,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAC/B,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;IAEnB,MAAM,OAAO,GAAG,KAAK,IAAI,EAAE;QACzB,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QACtB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,CAAC,CAAC,CAAA;YACpD,+BAA+B;QACjC,CAAC;IACH,CAAC,CAAA;IAED,OAAO;QACL,MAAM;QACN,MAAM,EAAE,YAAY;QACpB,OAAO;KACR,CAAA;AACH,CAAC"}
|
package/dist/mcp.js
CHANGED
|
@@ -1,491 +1,98 @@
|
|
|
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 { chromium } from '
|
|
4
|
+
import { chromium } from 'playwright-core';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import vm from 'node:vm';
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
10
11
|
const state = {
|
|
11
12
|
isConnected: false,
|
|
12
13
|
page: null,
|
|
13
14
|
browser: null,
|
|
14
|
-
|
|
15
|
-
consoleLogs: new Map(),
|
|
16
|
-
networkRequests: new Map(),
|
|
15
|
+
context: null,
|
|
17
16
|
};
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
async function isCDPAvailable() {
|
|
17
|
+
const RELAY_PORT = 19988;
|
|
18
|
+
async function isPortTaken(port) {
|
|
21
19
|
try {
|
|
22
|
-
const response = await fetch(`http://
|
|
20
|
+
const response = await fetch(`http://localhost:${port}/`);
|
|
23
21
|
return response.ok;
|
|
24
22
|
}
|
|
25
23
|
catch {
|
|
26
24
|
return false;
|
|
27
25
|
}
|
|
28
26
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
`--user-data-dir=${userDataDir}`,
|
|
39
|
-
'--no-first-run',
|
|
40
|
-
'--no-default-browser-check',
|
|
41
|
-
'--disable-session-crashed-bubble',
|
|
42
|
-
'--disable-features=DevToolsDebuggingRestrictions',
|
|
43
|
-
'--disable-blink-features=AutomationControlled',
|
|
44
|
-
'--no-sandbox',
|
|
45
|
-
'--disable-web-security',
|
|
46
|
-
'--disable-infobars',
|
|
47
|
-
'--disable-translate',
|
|
48
|
-
'--disable-features=AutomationControlled', // disables --enable-automation
|
|
49
|
-
'--disable-background-timer-throttling',
|
|
50
|
-
'--disable-popup-blocking',
|
|
51
|
-
'--disable-backgrounding-occluded-windows',
|
|
52
|
-
'--disable-renderer-backgrounding',
|
|
53
|
-
'--disable-window-activation',
|
|
54
|
-
'--disable-focus-on-load',
|
|
55
|
-
'--no-startup-window',
|
|
56
|
-
'--window-position=0,0',
|
|
57
|
-
'--disable-site-isolation-trials',
|
|
58
|
-
'--disable-features=IsolateOrigins,site-per-process',
|
|
59
|
-
];
|
|
60
|
-
const chromeProcess = spawn(executablePath, chromeArgs, {
|
|
27
|
+
async function ensureRelayServer() {
|
|
28
|
+
const portTaken = await isPortTaken(RELAY_PORT);
|
|
29
|
+
if (portTaken) {
|
|
30
|
+
console.error('CDP relay server already running');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.error('Starting CDP relay server...');
|
|
34
|
+
const scriptPath = require.resolve('../dist/start-relay-server.js');
|
|
35
|
+
const serverProcess = spawn(process.execPath, [scriptPath], {
|
|
61
36
|
detached: true,
|
|
62
37
|
stdio: 'ignore',
|
|
63
38
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return chromeProcess;
|
|
39
|
+
serverProcess.unref();
|
|
40
|
+
// wait for extension to connect
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
42
|
+
console.error('CDP relay server started');
|
|
69
43
|
}
|
|
70
|
-
// Ensure connection to Chrome via CDP
|
|
71
44
|
async function ensureConnection() {
|
|
72
45
|
if (state.isConnected && state.browser && state.page) {
|
|
73
46
|
return { browser: state.browser, page: state.page };
|
|
74
47
|
}
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
// Launch Chrome with CDP
|
|
79
|
-
const chromeProcess = await launchChromeWithCDP();
|
|
80
|
-
state.chromeProcess = chromeProcess;
|
|
81
|
-
}
|
|
82
|
-
// Connect to Chrome via CDP
|
|
83
|
-
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${CDP_PORT}`);
|
|
84
|
-
// Get the default context
|
|
48
|
+
await ensureRelayServer();
|
|
49
|
+
const cdpEndpoint = `ws://localhost:${RELAY_PORT}/cdp/${Date.now()}`;
|
|
50
|
+
const browser = await chromium.connectOverCDP(cdpEndpoint);
|
|
85
51
|
const contexts = browser.contexts();
|
|
86
|
-
|
|
87
|
-
if (contexts.length > 0) {
|
|
88
|
-
context = contexts[0];
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
context = await browser.newContext();
|
|
92
|
-
}
|
|
93
|
-
// Generate user agent and set it on context
|
|
94
|
-
const { default: UserAgent } = await import('user-agents');
|
|
95
|
-
const userAgent = new UserAgent({
|
|
96
|
-
platform: 'MacIntel',
|
|
97
|
-
deviceCategory: 'desktop',
|
|
98
|
-
});
|
|
99
|
-
// Get or create page
|
|
52
|
+
const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
|
|
100
53
|
const pages = context.pages();
|
|
101
|
-
|
|
102
|
-
if (pages.length > 0) {
|
|
103
|
-
page = pages[0];
|
|
104
|
-
// Set user agent on existing page
|
|
105
|
-
await page.setExtraHTTPHeaders({
|
|
106
|
-
'User-Agent': userAgent.toString(),
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
page = await context.newPage();
|
|
111
|
-
// Set user agent on new page
|
|
112
|
-
await page.setExtraHTTPHeaders({
|
|
113
|
-
'User-Agent': userAgent.toString(),
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
// Set up event listeners if not already set
|
|
117
|
-
if (!state.isConnected) {
|
|
118
|
-
page.on('console', (msg) => {
|
|
119
|
-
// Get or create logs array for this page
|
|
120
|
-
let pageLogs = state.consoleLogs.get(page);
|
|
121
|
-
if (!pageLogs) {
|
|
122
|
-
pageLogs = [];
|
|
123
|
-
state.consoleLogs.set(page, pageLogs);
|
|
124
|
-
}
|
|
125
|
-
// Add new log
|
|
126
|
-
pageLogs.push({
|
|
127
|
-
type: msg.type(),
|
|
128
|
-
text: msg.text(),
|
|
129
|
-
timestamp: Date.now(),
|
|
130
|
-
location: msg.location(),
|
|
131
|
-
});
|
|
132
|
-
// Keep only last 1000 logs
|
|
133
|
-
if (pageLogs.length > 1000) {
|
|
134
|
-
pageLogs.shift();
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
// Clean up logs and network requests when page is closed
|
|
138
|
-
page.on('close', () => {
|
|
139
|
-
state.consoleLogs.delete(page);
|
|
140
|
-
state.networkRequests.delete(page);
|
|
141
|
-
});
|
|
142
|
-
page.on('request', (request) => {
|
|
143
|
-
const startTime = Date.now();
|
|
144
|
-
const entry = {
|
|
145
|
-
url: request.url(),
|
|
146
|
-
method: request.method(),
|
|
147
|
-
headers: request.headers(),
|
|
148
|
-
timestamp: startTime,
|
|
149
|
-
};
|
|
150
|
-
request
|
|
151
|
-
.response()
|
|
152
|
-
.then((response) => {
|
|
153
|
-
if (response) {
|
|
154
|
-
entry.status = response.status();
|
|
155
|
-
entry.duration = Date.now() - startTime;
|
|
156
|
-
entry.size = response.headers()['content-length']
|
|
157
|
-
? parseInt(response.headers()['content-length'])
|
|
158
|
-
: 0;
|
|
159
|
-
// Get or create requests array for this page
|
|
160
|
-
let pageRequests = state.networkRequests.get(page);
|
|
161
|
-
if (!pageRequests) {
|
|
162
|
-
pageRequests = [];
|
|
163
|
-
state.networkRequests.set(page, pageRequests);
|
|
164
|
-
}
|
|
165
|
-
// Add new request
|
|
166
|
-
pageRequests.push(entry);
|
|
167
|
-
// Keep only last 1000 requests
|
|
168
|
-
if (pageRequests.length > 1000) {
|
|
169
|
-
pageRequests.shift();
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
})
|
|
173
|
-
.catch(() => {
|
|
174
|
-
// Handle response errors silently
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
}
|
|
54
|
+
const page = pages.length > 0 ? pages[0] : await context.newPage();
|
|
178
55
|
state.browser = browser;
|
|
179
56
|
state.page = page;
|
|
57
|
+
state.context = context;
|
|
180
58
|
state.isConnected = true;
|
|
181
59
|
return { browser, page };
|
|
182
60
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
title: 'Playwright MCP Server',
|
|
187
|
-
version: '1.0.0',
|
|
188
|
-
});
|
|
189
|
-
// Tool 1: New Page - Creates a new browser page
|
|
190
|
-
server.tool('new_page', 'Create a new browser page in the shared Chrome instance', {}, async () => {
|
|
191
|
-
try {
|
|
192
|
-
const { browser, page } = await ensureConnection();
|
|
193
|
-
// Always create a new page
|
|
194
|
-
const context = browser.contexts()[0] || await browser.newContext();
|
|
195
|
-
const newPage = await context.newPage();
|
|
196
|
-
// Set user agent on new page
|
|
197
|
-
const { default: UserAgent } = await import('user-agents');
|
|
198
|
-
const userAgent = new UserAgent({
|
|
199
|
-
platform: 'MacIntel',
|
|
200
|
-
deviceCategory: 'desktop',
|
|
201
|
-
});
|
|
202
|
-
await newPage.setExtraHTTPHeaders({
|
|
203
|
-
'User-Agent': userAgent.toString()
|
|
204
|
-
});
|
|
205
|
-
// Update state to use the new page
|
|
206
|
-
state.page = newPage;
|
|
207
|
-
// Set up event listeners on the new page
|
|
208
|
-
newPage.on('console', (msg) => {
|
|
209
|
-
// Get or create logs array for this page
|
|
210
|
-
let pageLogs = state.consoleLogs.get(newPage);
|
|
211
|
-
if (!pageLogs) {
|
|
212
|
-
pageLogs = [];
|
|
213
|
-
state.consoleLogs.set(newPage, pageLogs);
|
|
214
|
-
}
|
|
215
|
-
// Add new log
|
|
216
|
-
pageLogs.push({
|
|
217
|
-
type: msg.type(),
|
|
218
|
-
text: msg.text(),
|
|
219
|
-
timestamp: Date.now(),
|
|
220
|
-
location: msg.location(),
|
|
221
|
-
});
|
|
222
|
-
// Keep only last 1000 logs
|
|
223
|
-
if (pageLogs.length > 1000) {
|
|
224
|
-
pageLogs.shift();
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
// Clean up logs and network requests when page is closed
|
|
228
|
-
newPage.on('close', () => {
|
|
229
|
-
state.consoleLogs.delete(newPage);
|
|
230
|
-
state.networkRequests.delete(newPage);
|
|
231
|
-
});
|
|
232
|
-
newPage.on('request', (request) => {
|
|
233
|
-
const startTime = Date.now();
|
|
234
|
-
const entry = {
|
|
235
|
-
url: request.url(),
|
|
236
|
-
method: request.method(),
|
|
237
|
-
headers: request.headers(),
|
|
238
|
-
timestamp: startTime,
|
|
239
|
-
};
|
|
240
|
-
request
|
|
241
|
-
.response()
|
|
242
|
-
.then((response) => {
|
|
243
|
-
if (response) {
|
|
244
|
-
entry.status = response.status();
|
|
245
|
-
entry.duration = Date.now() - startTime;
|
|
246
|
-
entry.size = response.headers()['content-length']
|
|
247
|
-
? parseInt(response.headers()['content-length'])
|
|
248
|
-
: 0;
|
|
249
|
-
// Get or create requests array for this page
|
|
250
|
-
let pageRequests = state.networkRequests.get(newPage);
|
|
251
|
-
if (!pageRequests) {
|
|
252
|
-
pageRequests = [];
|
|
253
|
-
state.networkRequests.set(newPage, pageRequests);
|
|
254
|
-
}
|
|
255
|
-
// Add new request
|
|
256
|
-
pageRequests.push(entry);
|
|
257
|
-
// Keep only last 1000 requests
|
|
258
|
-
if (pageRequests.length > 1000) {
|
|
259
|
-
pageRequests.shift();
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
})
|
|
263
|
-
.catch(() => {
|
|
264
|
-
// Handle response errors silently
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
return {
|
|
268
|
-
content: [
|
|
269
|
-
{
|
|
270
|
-
type: 'text',
|
|
271
|
-
text: `Created new page. URL: ${newPage.url()}. Total pages: ${context.pages().length}`,
|
|
272
|
-
},
|
|
273
|
-
],
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
catch (error) {
|
|
277
|
-
return {
|
|
278
|
-
content: [
|
|
279
|
-
{
|
|
280
|
-
type: 'text',
|
|
281
|
-
text: `Failed to create new page: ${error.message}`,
|
|
282
|
-
},
|
|
283
|
-
],
|
|
284
|
-
isError: true,
|
|
285
|
-
};
|
|
61
|
+
async function getCurrentPage() {
|
|
62
|
+
if (state.page) {
|
|
63
|
+
return state.page;
|
|
286
64
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
.enum(['log', 'info', 'warning', 'error', 'debug'])
|
|
296
|
-
.optional()
|
|
297
|
-
.describe('Filter by message type'),
|
|
298
|
-
offset: z.number().default(0).describe('Start from this index'),
|
|
299
|
-
}, async ({ limit, type, offset }) => {
|
|
300
|
-
try {
|
|
301
|
-
const { page } = await ensureConnection(); // Ensure we're connected first
|
|
302
|
-
// Get logs for current page
|
|
303
|
-
const pageLogs = state.consoleLogs.get(page) || [];
|
|
304
|
-
// Filter and paginate logs
|
|
305
|
-
let logs = [...pageLogs];
|
|
306
|
-
if (type) {
|
|
307
|
-
logs = logs.filter((log) => log.type === type);
|
|
308
|
-
}
|
|
309
|
-
const paginatedLogs = logs.slice(offset, offset + limit);
|
|
310
|
-
// Format logs to look like Chrome console output
|
|
311
|
-
let consoleOutput = '';
|
|
312
|
-
if (paginatedLogs.length === 0) {
|
|
313
|
-
consoleOutput = 'No console messages';
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
consoleOutput = paginatedLogs
|
|
317
|
-
.map((log) => {
|
|
318
|
-
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
|
319
|
-
const location = log.location
|
|
320
|
-
? ` ${log.location.url}:${log.location.lineNumber}:${log.location.columnNumber}`
|
|
321
|
-
: '';
|
|
322
|
-
return `[${log.type}]: ${log.text}${location}`;
|
|
323
|
-
})
|
|
324
|
-
.join('\n');
|
|
325
|
-
if (logs.length > paginatedLogs.length) {
|
|
326
|
-
consoleOutput += `\n\n(Showing ${paginatedLogs.length} of ${logs.length} total messages)`;
|
|
65
|
+
if (state.browser) {
|
|
66
|
+
const contexts = state.browser.contexts();
|
|
67
|
+
if (contexts.length > 0) {
|
|
68
|
+
const pages = contexts[0].pages();
|
|
69
|
+
if (pages.length > 0) {
|
|
70
|
+
const page = pages[0];
|
|
71
|
+
await page.emulateMedia({ colorScheme: null });
|
|
72
|
+
return page;
|
|
327
73
|
}
|
|
328
74
|
}
|
|
329
|
-
return {
|
|
330
|
-
content: [
|
|
331
|
-
{
|
|
332
|
-
type: 'text',
|
|
333
|
-
text: consoleOutput,
|
|
334
|
-
},
|
|
335
|
-
],
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
catch (error) {
|
|
339
|
-
return {
|
|
340
|
-
content: [
|
|
341
|
-
{
|
|
342
|
-
type: 'text',
|
|
343
|
-
text: `Failed to get console logs: ${error.message}`,
|
|
344
|
-
},
|
|
345
|
-
],
|
|
346
|
-
isError: true,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
// Tool 3: Network History
|
|
351
|
-
server.tool('network_history', 'Get history of network requests', {
|
|
352
|
-
limit: z
|
|
353
|
-
.number()
|
|
354
|
-
.default(50)
|
|
355
|
-
.describe('Maximum number of requests to return'),
|
|
356
|
-
urlPattern: z
|
|
357
|
-
.string()
|
|
358
|
-
.optional()
|
|
359
|
-
.describe('Filter by URL pattern (supports wildcards)'),
|
|
360
|
-
method: z
|
|
361
|
-
.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
|
|
362
|
-
.optional()
|
|
363
|
-
.describe('Filter by HTTP method'),
|
|
364
|
-
statusCode: z
|
|
365
|
-
.object({
|
|
366
|
-
min: z.number().optional(),
|
|
367
|
-
max: z.number().optional(),
|
|
368
|
-
})
|
|
369
|
-
.optional()
|
|
370
|
-
.describe('Filter by status code range'),
|
|
371
|
-
includeBody: z
|
|
372
|
-
.boolean()
|
|
373
|
-
.default(false)
|
|
374
|
-
.describe('Include request/response bodies'),
|
|
375
|
-
}, async ({ limit, urlPattern, method, statusCode, includeBody }) => {
|
|
376
|
-
try {
|
|
377
|
-
const { page } = await ensureConnection();
|
|
378
|
-
// Get requests for current page
|
|
379
|
-
const pageRequests = state.networkRequests.get(page) || [];
|
|
380
|
-
// If includeBody is requested, we need to fetch bodies for existing requests
|
|
381
|
-
if (includeBody && pageRequests.length > 0) {
|
|
382
|
-
// Note: In a real implementation, you'd store bodies during capture
|
|
383
|
-
console.warn('Body capture not implemented in this example');
|
|
384
|
-
}
|
|
385
|
-
// Filter requests
|
|
386
|
-
let requests = [...pageRequests];
|
|
387
|
-
if (urlPattern) {
|
|
388
|
-
const pattern = new RegExp(urlPattern.replace(/\*/g, '.*'));
|
|
389
|
-
requests = requests.filter((req) => pattern.test(req.url));
|
|
390
|
-
}
|
|
391
|
-
if (method) {
|
|
392
|
-
requests = requests.filter((req) => req.method === method);
|
|
393
|
-
}
|
|
394
|
-
if (statusCode) {
|
|
395
|
-
requests = requests.filter((req) => {
|
|
396
|
-
if (statusCode.min && req.status < statusCode.min)
|
|
397
|
-
return false;
|
|
398
|
-
if (statusCode.max && req.status > statusCode.max)
|
|
399
|
-
return false;
|
|
400
|
-
return true;
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
const limitedRequests = requests.slice(-limit);
|
|
404
|
-
return {
|
|
405
|
-
content: [
|
|
406
|
-
{
|
|
407
|
-
type: 'text',
|
|
408
|
-
text: JSON.stringify({
|
|
409
|
-
total: requests.length,
|
|
410
|
-
requests: limitedRequests,
|
|
411
|
-
}, null, 2),
|
|
412
|
-
},
|
|
413
|
-
],
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
catch (error) {
|
|
417
|
-
return {
|
|
418
|
-
content: [
|
|
419
|
-
{
|
|
420
|
-
type: 'text',
|
|
421
|
-
text: `Failed to get network history: ${error.message}`,
|
|
422
|
-
},
|
|
423
|
-
],
|
|
424
|
-
isError: true,
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
// Tool 4: Accessibility Snapshot - Get page accessibility tree as JSON
|
|
429
|
-
server.tool('accessibility_snapshot', 'Get the accessibility snapshot of the current page as JSON', {}, async ({}) => {
|
|
430
|
-
try {
|
|
431
|
-
const { page } = await ensureConnection();
|
|
432
|
-
// Check if the method exists
|
|
433
|
-
if (typeof page._snapshotForAI !== 'function') {
|
|
434
|
-
// Fall back to regular accessibility snapshot
|
|
435
|
-
const snapshot = await page.accessibility.snapshot({
|
|
436
|
-
interestingOnly: true,
|
|
437
|
-
root: undefined,
|
|
438
|
-
});
|
|
439
|
-
return {
|
|
440
|
-
content: [
|
|
441
|
-
{
|
|
442
|
-
type: 'text',
|
|
443
|
-
text: JSON.stringify(snapshot, null, 2),
|
|
444
|
-
},
|
|
445
|
-
],
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
const snapshot = await page._snapshotForAI();
|
|
449
|
-
return {
|
|
450
|
-
content: [
|
|
451
|
-
{
|
|
452
|
-
type: 'text',
|
|
453
|
-
text: snapshot,
|
|
454
|
-
},
|
|
455
|
-
],
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
catch (error) {
|
|
459
|
-
console.error('Accessibility snapshot error:', error);
|
|
460
|
-
return {
|
|
461
|
-
content: [
|
|
462
|
-
{
|
|
463
|
-
type: 'text',
|
|
464
|
-
text: `Failed to get accessibility snapshot: ${error.message}`,
|
|
465
|
-
},
|
|
466
|
-
],
|
|
467
|
-
isError: true,
|
|
468
|
-
};
|
|
469
75
|
}
|
|
76
|
+
throw new Error('No page available');
|
|
77
|
+
}
|
|
78
|
+
const server = new McpServer({
|
|
79
|
+
name: 'playwriter',
|
|
80
|
+
title: 'Playwright MCP Server',
|
|
81
|
+
version: '1.0.0',
|
|
470
82
|
});
|
|
471
|
-
// Tool 5: Execute - Run arbitrary JavaScript code with page and context in scope
|
|
472
83
|
const promptContent = fs.readFileSync(path.join(path.dirname(new URL(import.meta.url).pathname), 'prompt.md'), 'utf-8');
|
|
473
84
|
server.tool('execute', promptContent, {
|
|
474
85
|
code: z
|
|
475
86
|
.string()
|
|
476
87
|
.describe('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. '),
|
|
477
|
-
timeout: z
|
|
478
|
-
.number()
|
|
479
|
-
.default(3000)
|
|
480
|
-
.describe('Timeout in milliseconds for code execution (default: 3000ms)'),
|
|
88
|
+
timeout: z.number().default(3000).describe('Timeout in milliseconds for code execution (default: 3000ms)'),
|
|
481
89
|
}, async ({ code, timeout }) => {
|
|
482
|
-
|
|
483
|
-
const
|
|
90
|
+
await ensureConnection();
|
|
91
|
+
const page = await getCurrentPage();
|
|
92
|
+
const context = state.context || page.context();
|
|
484
93
|
console.error('Executing code:', code);
|
|
485
94
|
try {
|
|
486
|
-
// Collect console logs during execution
|
|
487
95
|
const consoleLogs = [];
|
|
488
|
-
// Create a custom console object that collects logs
|
|
489
96
|
const customConsole = {
|
|
490
97
|
log: (...args) => {
|
|
491
98
|
consoleLogs.push({ method: 'log', args });
|
|
@@ -503,20 +110,40 @@ server.tool('execute', promptContent, {
|
|
|
503
110
|
consoleLogs.push({ method: 'debug', args });
|
|
504
111
|
},
|
|
505
112
|
};
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
113
|
+
const accessibilitySnapshot = async (targetPage) => {
|
|
114
|
+
if (targetPage._snapshotForAI) {
|
|
115
|
+
const snapshot = await targetPage._snapshotForAI();
|
|
116
|
+
const snapshotStr = typeof snapshot === 'string'
|
|
117
|
+
? snapshot
|
|
118
|
+
: JSON.stringify(snapshot, null, 2);
|
|
119
|
+
return snapshotStr;
|
|
120
|
+
}
|
|
121
|
+
throw new Error('accessibilitySnapshot is not available on this page');
|
|
122
|
+
};
|
|
123
|
+
const activateTab = async (targetPage) => {
|
|
124
|
+
const cdp = await context.newCDPSession(targetPage);
|
|
125
|
+
const { targetInfo } = await cdp.send('Target.getTargetInfo');
|
|
126
|
+
const targetId = targetInfo.targetId;
|
|
127
|
+
await cdp.send('Playwriter.activateTab', { targetId });
|
|
128
|
+
await cdp.detach();
|
|
129
|
+
};
|
|
130
|
+
const vmContext = vm.createContext({
|
|
131
|
+
page,
|
|
132
|
+
context,
|
|
133
|
+
state,
|
|
134
|
+
console: customConsole,
|
|
135
|
+
accessibilitySnapshot,
|
|
136
|
+
activateTab,
|
|
137
|
+
});
|
|
138
|
+
const wrappedCode = `(async () => { ${code} })()`;
|
|
513
139
|
const result = await Promise.race([
|
|
514
|
-
|
|
515
|
-
|
|
140
|
+
vm.runInContext(wrappedCode, vmContext, {
|
|
141
|
+
timeout,
|
|
142
|
+
displayErrors: true,
|
|
143
|
+
}),
|
|
144
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Code execution timed out after ${timeout}ms`)), timeout)),
|
|
516
145
|
]);
|
|
517
|
-
// Format the response with both console output and return value
|
|
518
146
|
let responseText = '';
|
|
519
|
-
// Add console logs if any
|
|
520
147
|
if (consoleLogs.length > 0) {
|
|
521
148
|
responseText += 'Console output:\n';
|
|
522
149
|
consoleLogs.forEach(({ method, args }) => {
|
|
@@ -532,19 +159,28 @@ server.tool('execute', promptContent, {
|
|
|
532
159
|
});
|
|
533
160
|
responseText += '\n';
|
|
534
161
|
}
|
|
535
|
-
// Add return value if any
|
|
536
162
|
if (result !== undefined) {
|
|
537
163
|
responseText += 'Return value:\n';
|
|
538
|
-
|
|
164
|
+
if (typeof result === 'string') {
|
|
165
|
+
responseText += result;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
responseText += JSON.stringify(result, null, 2);
|
|
169
|
+
}
|
|
539
170
|
}
|
|
540
171
|
else if (consoleLogs.length === 0) {
|
|
541
172
|
responseText += 'Code executed successfully (no output)';
|
|
542
173
|
}
|
|
174
|
+
const MAX_LENGTH = 1000;
|
|
175
|
+
let finalText = responseText.trim();
|
|
176
|
+
if (finalText.length > MAX_LENGTH) {
|
|
177
|
+
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]`;
|
|
178
|
+
}
|
|
543
179
|
return {
|
|
544
180
|
content: [
|
|
545
181
|
{
|
|
546
182
|
type: 'text',
|
|
547
|
-
text:
|
|
183
|
+
text: finalText,
|
|
548
184
|
},
|
|
549
185
|
],
|
|
550
186
|
};
|
|
@@ -554,7 +190,7 @@ server.tool('execute', promptContent, {
|
|
|
554
190
|
content: [
|
|
555
191
|
{
|
|
556
192
|
type: 'text',
|
|
557
|
-
text: `Error executing code: ${error.message}\n${error.stack}`,
|
|
193
|
+
text: `Error executing code: ${error.message}\n${error.stack || ''}`,
|
|
558
194
|
},
|
|
559
195
|
],
|
|
560
196
|
isError: true,
|
|
@@ -567,23 +203,16 @@ async function main() {
|
|
|
567
203
|
await server.connect(transport);
|
|
568
204
|
console.error('Playwright MCP server running on stdio');
|
|
569
205
|
}
|
|
570
|
-
// Cleanup function
|
|
571
206
|
async function cleanup() {
|
|
572
207
|
console.error('Shutting down MCP server...');
|
|
573
208
|
if (state.browser) {
|
|
574
209
|
try {
|
|
575
|
-
// Close the browser connection but not the Chrome process
|
|
576
|
-
// Since we're using CDP, closing the browser object just closes
|
|
577
|
-
// the connection, not the actual Chrome instance
|
|
578
210
|
await state.browser.close();
|
|
579
211
|
}
|
|
580
212
|
catch (e) {
|
|
581
213
|
// Ignore errors during browser close
|
|
582
214
|
}
|
|
583
215
|
}
|
|
584
|
-
// Don't kill the Chrome process - let it continue running
|
|
585
|
-
// The process was started with detached: true and unref()
|
|
586
|
-
// so it will persist after this process exits
|
|
587
216
|
process.exit(0);
|
|
588
217
|
}
|
|
589
218
|
// Handle process termination
|