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/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 ua = require('user-agents');
|
|
95
|
-
const userAgent = new ua({
|
|
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 ua = require('user-agents');
|
|
198
|
-
const userAgent = new ua({
|
|
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
|
-
};
|
|
61
|
+
async function getCurrentPage() {
|
|
62
|
+
if (state.page) {
|
|
63
|
+
return state.page;
|
|
275
64
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
isError: true,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
// Tool 2: Console Logs
|
|
289
|
-
server.tool('console_logs', 'Retrieve console messages from the page', {
|
|
290
|
-
limit: z
|
|
291
|
-
.number()
|
|
292
|
-
.default(50)
|
|
293
|
-
.describe('Maximum number of messages to return'),
|
|
294
|
-
type: z
|
|
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,21 @@ server.tool('execute', promptContent, {
|
|
|
503
110
|
consoleLogs.push({ method: 'debug', args });
|
|
504
111
|
},
|
|
505
112
|
};
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
113
|
+
const vmContext = vm.createContext({
|
|
114
|
+
page,
|
|
115
|
+
context,
|
|
116
|
+
state,
|
|
117
|
+
console: customConsole,
|
|
118
|
+
});
|
|
119
|
+
const wrappedCode = `(async () => { ${code} })()`;
|
|
513
120
|
const result = await Promise.race([
|
|
514
|
-
|
|
515
|
-
|
|
121
|
+
vm.runInContext(wrappedCode, vmContext, {
|
|
122
|
+
timeout,
|
|
123
|
+
displayErrors: true,
|
|
124
|
+
}),
|
|
125
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Code execution timed out after ${timeout}ms`)), timeout)),
|
|
516
126
|
]);
|
|
517
|
-
// Format the response with both console output and return value
|
|
518
127
|
let responseText = '';
|
|
519
|
-
// Add console logs if any
|
|
520
128
|
if (consoleLogs.length > 0) {
|
|
521
129
|
responseText += 'Console output:\n';
|
|
522
130
|
consoleLogs.forEach(({ method, args }) => {
|
|
@@ -532,19 +140,28 @@ server.tool('execute', promptContent, {
|
|
|
532
140
|
});
|
|
533
141
|
responseText += '\n';
|
|
534
142
|
}
|
|
535
|
-
// Add return value if any
|
|
536
143
|
if (result !== undefined) {
|
|
537
144
|
responseText += 'Return value:\n';
|
|
538
|
-
|
|
145
|
+
if (typeof result === 'string') {
|
|
146
|
+
responseText += result;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
responseText += JSON.stringify(result, null, 2);
|
|
150
|
+
}
|
|
539
151
|
}
|
|
540
152
|
else if (consoleLogs.length === 0) {
|
|
541
153
|
responseText += 'Code executed successfully (no output)';
|
|
542
154
|
}
|
|
155
|
+
const MAX_LENGTH = 1000;
|
|
156
|
+
let finalText = responseText.trim();
|
|
157
|
+
if (finalText.length > MAX_LENGTH) {
|
|
158
|
+
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]`;
|
|
159
|
+
}
|
|
543
160
|
return {
|
|
544
161
|
content: [
|
|
545
162
|
{
|
|
546
163
|
type: 'text',
|
|
547
|
-
text:
|
|
164
|
+
text: finalText,
|
|
548
165
|
},
|
|
549
166
|
],
|
|
550
167
|
};
|
|
@@ -554,7 +171,7 @@ server.tool('execute', promptContent, {
|
|
|
554
171
|
content: [
|
|
555
172
|
{
|
|
556
173
|
type: 'text',
|
|
557
|
-
text: `Error executing code: ${error.message}\n${error.stack}`,
|
|
174
|
+
text: `Error executing code: ${error.message}\n${error.stack || ''}`,
|
|
558
175
|
},
|
|
559
176
|
],
|
|
560
177
|
isError: true,
|
|
@@ -567,23 +184,16 @@ async function main() {
|
|
|
567
184
|
await server.connect(transport);
|
|
568
185
|
console.error('Playwright MCP server running on stdio');
|
|
569
186
|
}
|
|
570
|
-
// Cleanup function
|
|
571
187
|
async function cleanup() {
|
|
572
188
|
console.error('Shutting down MCP server...');
|
|
573
189
|
if (state.browser) {
|
|
574
190
|
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
191
|
await state.browser.close();
|
|
579
192
|
}
|
|
580
193
|
catch (e) {
|
|
581
194
|
// Ignore errors during browser close
|
|
582
195
|
}
|
|
583
196
|
}
|
|
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
197
|
process.exit(0);
|
|
588
198
|
}
|
|
589
199
|
// Handle process termination
|