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.
Files changed (46) hide show
  1. package/bin.js +1 -1
  2. package/dist/browser-config.js +1 -3
  3. package/dist/browser-config.js.map +1 -1
  4. package/dist/cdp-types.d.ts +25 -0
  5. package/dist/cdp-types.d.ts.map +1 -0
  6. package/dist/cdp-types.js +91 -0
  7. package/dist/cdp-types.js.map +1 -0
  8. package/dist/extension/cdp-relay.d.ts +12 -0
  9. package/dist/extension/cdp-relay.d.ts.map +1 -0
  10. package/dist/extension/cdp-relay.js +384 -0
  11. package/dist/extension/cdp-relay.js.map +1 -0
  12. package/dist/extension/protocol.d.ts +35 -0
  13. package/dist/extension/protocol.d.ts.map +1 -0
  14. package/dist/extension/protocol.js +2 -0
  15. package/dist/extension/protocol.js.map +1 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/mcp-client.d.ts.map +1 -1
  21. package/dist/mcp-client.js +1 -1
  22. package/dist/mcp-client.js.map +1 -1
  23. package/dist/mcp.js +93 -464
  24. package/dist/mcp.js.map +1 -1
  25. package/dist/mcp.test.js +101 -142
  26. package/dist/mcp.test.js.map +1 -1
  27. package/dist/prompt.md +45 -491
  28. package/dist/resource.md +436 -0
  29. package/dist/start-relay-server.d.ts +8 -0
  30. package/dist/start-relay-server.d.ts.map +1 -0
  31. package/dist/start-relay-server.js +33 -0
  32. package/dist/start-relay-server.js.map +1 -0
  33. package/package.json +42 -36
  34. package/src/browser-config.ts +48 -50
  35. package/src/cdp-types.ts +124 -0
  36. package/src/extension/cdp-relay.ts +487 -0
  37. package/src/extension/protocol.ts +41 -0
  38. package/src/index.ts +1 -0
  39. package/src/mcp-client.ts +46 -46
  40. package/src/mcp.test.ts +109 -165
  41. package/src/mcp.ts +223 -693
  42. package/src/prompt.md +45 -491
  43. package/src/resource.md +436 -0
  44. package/src/snapshots/hacker-news-initial-accessibility.md +243 -127
  45. package/src/snapshots/shadcn-ui-accessibility.md +300 -510
  46. package/src/start-relay-server.ts +43 -0
@@ -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;IAIhD,MAAM,SAAS,GAAG,IAAI,oBAAoB,CAAC;QACvC,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;YACD,GAAG,OAAO,CAAC,GAAG;YACd,KAAK,EAAE,qBAAqB;YAC5B,YAAY,EAAE,GAAG;YACjB,eAAe,EAAE,GAAG;SACvB;KACJ,CAAC,CAAA;IAEF,OAAO;QACH,SAAS;QACT,MAAM,EAAE,SAAS,CAAC,MAAO;KAC5B,CAAA;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAgC;IAKlE,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC;QACtB,IAAI,EAAE,OAAO,EAAE,UAAU,IAAI,MAAM;QACnC,OAAO,EAAE,OAAO;KACnB,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;QACxB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAE1B,YAAY,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAA;IACnC,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;QACvB,IAAI,CAAC;YACD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QACxB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,CAAC,CAAC,CAAA;YAClD,+BAA+B;QACnC,CAAC;IACL,CAAC,CAAA;IAED,OAAO;QACH,MAAM;QACN,MAAM,EAAE,YAAY;QACpB,OAAO;KACV,CAAA;AACL,CAAC"}
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 'patchright-core';
4
+ import { chromium } from 'playwright-core';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import os from 'node:os';
8
- import { spawn } from 'child_process';
9
- import { getBrowserExecutablePath } from './browser-config.js';
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
- chromeProcess: null,
15
- consoleLogs: new Map(),
16
- networkRequests: new Map(),
15
+ context: null,
17
16
  };
18
- const CDP_PORT = 9922;
19
- // Check if CDP is available on the specified port
20
- async function isCDPAvailable() {
17
+ const RELAY_PORT = 19988;
18
+ async function isPortTaken(port) {
21
19
  try {
22
- const response = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`);
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
- // Launch Chrome with CDP enabled
30
- async function launchChromeWithCDP() {
31
- const userDataDir = path.join(os.homedir(), '.playwriter');
32
- if (!fs.existsSync(userDataDir)) {
33
- fs.mkdirSync(userDataDir, { recursive: true });
34
- }
35
- const executablePath = getBrowserExecutablePath();
36
- const chromeArgs = [
37
- `--remote-debugging-port=${CDP_PORT}`,
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
- // Unref the process so it doesn't keep the parent process alive
65
- chromeProcess.unref();
66
- // Give Chrome time to start up
67
- await new Promise((resolve) => setTimeout(resolve, 2000));
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
- // Check if CDP is already available
76
- const cdpAvailable = await isCDPAvailable();
77
- if (!cdpAvailable) {
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
- let context;
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
- let page;
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
- // Initialize MCP server
184
- const server = new McpServer({
185
- name: 'playwriter',
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
- // 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
- const { page } = await ensureConnection();
483
- const context = page.context();
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
- // Create a function that has page, context, and console in scope
507
- const executeCode = new Function('page', 'context', 'console', `
508
- return (async () => {
509
- ${code}
510
- })();
511
- `);
512
- // Execute the code with page, context, and custom console with timeout
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
- executeCode(page, context, customConsole),
515
- new Promise((_, reject) => setTimeout(() => reject(new Error(`Code execution timed out after ${timeout}ms`)), timeout))
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
- responseText += JSON.stringify(result, null, 2);
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: responseText.trim(),
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