@wordbricks/playwright-mcp 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/cli-wrapper.js +15 -14
  2. package/cli.js +1 -1
  3. package/config.d.ts +11 -6
  4. package/index.d.ts +7 -5
  5. package/index.js +1 -1
  6. package/lib/browserContextFactory.js +131 -58
  7. package/lib/browserServerBackend.js +14 -12
  8. package/lib/config.js +60 -46
  9. package/lib/context.js +41 -39
  10. package/lib/extension/cdpRelay.js +67 -61
  11. package/lib/extension/extensionContextFactory.js +10 -10
  12. package/lib/frameworkPatterns.js +21 -21
  13. package/lib/hooks/antiBotDetectionHook.js +59 -52
  14. package/lib/hooks/core.js +11 -10
  15. package/lib/hooks/eventConsumer.js +21 -21
  16. package/lib/hooks/events.js +3 -3
  17. package/lib/hooks/formatToolCallEvent.js +3 -7
  18. package/lib/hooks/frameworkStateHook.js +40 -40
  19. package/lib/hooks/grouping.js +3 -3
  20. package/lib/hooks/jsonLdDetectionHook.js +44 -37
  21. package/lib/hooks/networkFilters.js +17 -17
  22. package/lib/hooks/networkSetup.js +9 -7
  23. package/lib/hooks/networkTrackingHook.js +21 -21
  24. package/lib/hooks/pageHeightHook.js +9 -9
  25. package/lib/hooks/registry.js +15 -16
  26. package/lib/hooks/requireTabHook.js +3 -3
  27. package/lib/hooks/schema.js +38 -38
  28. package/lib/hooks/waitHook.js +7 -7
  29. package/lib/index.js +12 -10
  30. package/lib/mcp/inProcessTransport.js +3 -4
  31. package/lib/mcp/proxyBackend.js +43 -28
  32. package/lib/mcp/server.js +24 -19
  33. package/lib/mcp/tool.js +14 -8
  34. package/lib/mcp/transport.js +60 -53
  35. package/lib/playwrightTransformer.js +129 -106
  36. package/lib/program.js +54 -52
  37. package/lib/response.js +36 -30
  38. package/lib/sessionLog.js +19 -17
  39. package/lib/tab.js +41 -39
  40. package/lib/tools/common.js +19 -19
  41. package/lib/tools/console.js +11 -11
  42. package/lib/tools/dialogs.js +18 -15
  43. package/lib/tools/evaluate.js +26 -17
  44. package/lib/tools/extractFrameworkState.js +48 -37
  45. package/lib/tools/files.js +17 -14
  46. package/lib/tools/form.js +32 -23
  47. package/lib/tools/getSnapshot.js +14 -15
  48. package/lib/tools/getVisibleHtml.js +33 -17
  49. package/lib/tools/install.js +20 -20
  50. package/lib/tools/keyboard.js +29 -24
  51. package/lib/tools/mouse.js +29 -31
  52. package/lib/tools/navigate.js +19 -23
  53. package/lib/tools/network.js +12 -14
  54. package/lib/tools/networkDetail.js +58 -49
  55. package/lib/tools/networkSearch/bodySearch.js +46 -32
  56. package/lib/tools/networkSearch/grouping.js +15 -6
  57. package/lib/tools/networkSearch/helpers.js +4 -4
  58. package/lib/tools/networkSearch/searchHtml.js +25 -16
  59. package/lib/tools/networkSearch/urlSearch.js +56 -14
  60. package/lib/tools/networkSearch.js +46 -36
  61. package/lib/tools/pdf.js +13 -12
  62. package/lib/tools/repl.js +66 -54
  63. package/lib/tools/screenshot.js +57 -33
  64. package/lib/tools/scroll.js +29 -24
  65. package/lib/tools/snapshot.js +66 -49
  66. package/lib/tools/tabs.js +22 -19
  67. package/lib/tools/tool.js +5 -3
  68. package/lib/tools/utils.js +17 -13
  69. package/lib/tools/wait.js +24 -19
  70. package/lib/tools.js +21 -20
  71. package/lib/utils/adBlockFilter.js +29 -26
  72. package/lib/utils/codegen.js +20 -16
  73. package/lib/utils/extensionPath.js +4 -4
  74. package/lib/utils/fileUtils.js +17 -13
  75. package/lib/utils/graphql.js +69 -58
  76. package/lib/utils/guid.js +3 -3
  77. package/lib/utils/httpServer.js +9 -9
  78. package/lib/utils/log.js +3 -3
  79. package/lib/utils/manualPromise.js +7 -7
  80. package/lib/utils/networkFormat.js +7 -5
  81. package/lib/utils/package.js +4 -4
  82. package/lib/utils/sanitizeHtml.js +66 -34
  83. package/lib/utils/truncate.js +25 -25
  84. package/lib/utils/withTimeout.js +1 -1
  85. package/package.json +34 -57
  86. package/src/index.ts +27 -17
  87. package/LICENSE +0 -202
package/lib/mcp/server.js CHANGED
@@ -13,13 +13,13 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import debug from 'debug';
17
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
- import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
19
- import ms from 'ms';
20
- import { ManualPromise } from '../utils/manualPromise.js';
21
- import { logUnhandledError } from '../utils/log.js';
22
- const serverDebug = debug('pw:mcp:server');
16
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
17
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
18
+ import debug from "debug";
19
+ import ms from "ms";
20
+ import { logUnhandledError } from "../utils/log.js";
21
+ import { ManualPromise } from "../utils/manualPromise.js";
22
+ const serverDebug = debug("pw:mcp:server");
23
23
  export async function connect(serverBackendFactory, transport, runHeartbeat) {
24
24
  const backend = serverBackendFactory();
25
25
  const server = createServer(backend, runHeartbeat);
@@ -31,17 +31,17 @@ export function createServer(backend, runHeartbeat) {
31
31
  const server = new Server({ name: backend.name, version: backend.version }, {
32
32
  capabilities: {
33
33
  tools: {},
34
- }
34
+ },
35
35
  });
36
36
  server.setRequestHandler(ListToolsRequestSchema, async () => {
37
- serverDebug('listTools');
37
+ serverDebug("listTools");
38
38
  await initializedPromise;
39
39
  const tools = await backend.listTools();
40
40
  return { tools };
41
41
  });
42
42
  let heartbeatRunning = false;
43
43
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
44
- serverDebug('callTool', request);
44
+ serverDebug("callTool", request);
45
45
  await initializedPromise;
46
46
  if (runHeartbeat && !heartbeatRunning) {
47
47
  heartbeatRunning = true;
@@ -52,26 +52,31 @@ export function createServer(backend, runHeartbeat) {
52
52
  }
53
53
  catch (error) {
54
54
  return {
55
- content: [{ type: 'text', text: '### Result\n' + String(error) }],
55
+ content: [{ type: "text", text: "### Result\n" + String(error) }],
56
56
  isError: true,
57
57
  };
58
58
  }
59
59
  });
60
- addServerListener(server, 'initialized', () => {
61
- backend.initialize?.(server).then(() => initializedPromise.resolve()).catch(logUnhandledError);
60
+ addServerListener(server, "initialized", () => {
61
+ backend
62
+ .initialize?.(server)
63
+ .then(() => initializedPromise.resolve())
64
+ .catch(logUnhandledError);
62
65
  });
63
- addServerListener(server, 'close', () => backend.serverClosed?.());
66
+ addServerListener(server, "close", () => backend.serverClosed?.());
64
67
  return server;
65
68
  }
66
69
  const startHeartbeat = (server) => {
67
70
  const beat = () => {
68
- serverDebug('Health check...');
71
+ serverDebug("Health check...");
69
72
  Promise.race([
70
73
  server.ping(),
71
- new Promise((_, reject) => setTimeout(() => reject(new Error('ping timeout')), ms('5s'))),
72
- ]).then(() => {
73
- setTimeout(beat, ms('3s'));
74
- }).catch(() => {
74
+ new Promise((_, reject) => setTimeout(() => reject(new Error("ping timeout")), ms("5s"))),
75
+ ])
76
+ .then(() => {
77
+ setTimeout(beat, ms("3s"));
78
+ })
79
+ .catch(() => {
75
80
  void server.close();
76
81
  });
77
82
  };
package/lib/mcp/tool.js CHANGED
@@ -13,18 +13,24 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { zodToJsonSchema } from 'zod-to-json-schema';
17
- import { z } from 'zod';
18
- const typesWithIntent = ['action', 'assertion', 'input'];
16
+ import { z } from "zod";
17
+ import { zodToJsonSchema } from "zod-to-json-schema";
18
+ const typesWithIntent = ["action", "assertion", "input"];
19
19
  export function toMcpTool(tool, options) {
20
- const inputSchema = options?.addIntent && typesWithIntent.includes(tool.type) ? tool.inputSchema.extend({
21
- intent: z.string().describe('The intent of the call, for example the test step description plan idea')
22
- }) : tool.inputSchema;
23
- const readOnly = tool.type === 'readOnly' || tool.type === 'assertion';
20
+ const inputSchema = options?.addIntent && typesWithIntent.includes(tool.type)
21
+ ? tool.inputSchema.extend({
22
+ intent: z
23
+ .string()
24
+ .describe("The intent of the call, for example the test step description plan idea"),
25
+ })
26
+ : tool.inputSchema;
27
+ const readOnly = tool.type === "readOnly" || tool.type === "assertion";
24
28
  return {
25
29
  name: tool.name,
26
30
  description: tool.description,
27
- inputSchema: zodToJsonSchema(inputSchema, { strictUnions: true }),
31
+ inputSchema: zodToJsonSchema(inputSchema, {
32
+ strictUnions: true,
33
+ }),
28
34
  annotations: {
29
35
  title: tool.title,
30
36
  readOnlyHint: readOnly,
@@ -13,19 +13,19 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import crypto from 'crypto';
17
- import debug from 'debug';
18
- import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
19
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
20
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
21
- import { SUPPORTED_PROTOCOL_VERSIONS, LATEST_PROTOCOL_VERSION, isInitializeRequest, isJSONRPCRequest } from '@modelcontextprotocol/sdk/types.js';
22
- import contentType from 'content-type';
23
- import getRawBody from 'raw-body';
24
- import { pipe } from '@fxts/core';
25
- import { httpAddressToString, startHttpServer } from '../utils/httpServer.js';
26
- import * as mcpServer from './server.js';
16
+ import { pipe } from "@fxts/core";
17
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
18
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
20
+ import { isInitializeRequest, isJSONRPCRequest, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, } from "@modelcontextprotocol/sdk/types.js";
21
+ import contentType from "content-type";
22
+ import crypto from "crypto";
23
+ import debug from "debug";
24
+ import getRawBody from "raw-body";
25
+ import { httpAddressToString, startHttpServer } from "../utils/httpServer.js";
26
+ import * as mcpServer from "./server.js";
27
27
  // @see node_modules/@modelcontextprotocol/sdk/dist/esm/server/streamableHttp.js
28
- const MAXIMUM_MESSAGE_SIZE = '4mb';
28
+ const MAXIMUM_MESSAGE_SIZE = "4mb";
29
29
  export async function start(serverBackendFactory, options) {
30
30
  if (options.port !== undefined) {
31
31
  const httpServer = await startHttpServer(options);
@@ -38,17 +38,17 @@ export async function start(serverBackendFactory, options) {
38
38
  async function startStdioTransport(serverBackendFactory) {
39
39
  await mcpServer.connect(serverBackendFactory, new StdioServerTransport(), false);
40
40
  }
41
- const testDebug = debug('pw:mcp:test');
41
+ const testDebug = debug("pw:mcp:test");
42
42
  async function handleStreamableReinitiate(req, res, transport, serverBackendFactory, sessionId, serverInfos) {
43
- const ct = req.headers['content-type'];
44
- if (!ct || !ct.includes('application/json')) {
43
+ const ct = req.headers["content-type"];
44
+ if (!ct || !ct.includes("application/json")) {
45
45
  res.writeHead(415).end(JSON.stringify({
46
- jsonrpc: '2.0',
46
+ jsonrpc: "2.0",
47
47
  error: {
48
48
  code: -32000,
49
- message: 'Unsupported Media Type: Content-Type must be application/json'
49
+ message: "Unsupported Media Type: Content-Type must be application/json",
50
50
  },
51
- id: null
51
+ id: null,
52
52
  }));
53
53
  return;
54
54
  }
@@ -57,28 +57,35 @@ async function handleStreamableReinitiate(req, res, transport, serverBackendFact
57
57
  const body = await pipe(getRawBody(req, {
58
58
  limit: MAXIMUM_MESSAGE_SIZE,
59
59
  encoding,
60
- }), raw => raw.toString(), JSON.parse);
61
- const msg = Array.isArray(body) ? (body.length === 1 ? body[0] : undefined) : body;
60
+ }), (raw) => raw.toString(), JSON.parse);
61
+ const msg = Array.isArray(body)
62
+ ? body.length === 1
63
+ ? body[0]
64
+ : undefined
65
+ : body;
62
66
  if (body && isJSONRPCRequest(msg) && isInitializeRequest(msg)) {
63
67
  const requestedVersion = msg.params.protocolVersion;
64
- const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion : LATEST_PROTOCOL_VERSION;
68
+ const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
69
+ ? requestedVersion
70
+ : LATEST_PROTOCOL_VERSION;
65
71
  const headers = {
66
- 'Content-Type': 'text/event-stream',
67
- 'Mcp-Session-Id': transport.sessionId
72
+ "Content-Type": "text/event-stream",
73
+ "Mcp-Session-Id": transport.sessionId,
68
74
  };
69
75
  res.writeHead(200, headers);
70
- const info = serverInfos.get(sessionId) || (() => {
71
- const backend = serverBackendFactory();
72
- return { name: backend.name, version: backend.version };
73
- })();
76
+ const info = serverInfos.get(sessionId) ||
77
+ (() => {
78
+ const backend = serverBackendFactory();
79
+ return { name: backend.name, version: backend.version };
80
+ })();
74
81
  const response = {
75
- jsonrpc: '2.0',
82
+ jsonrpc: "2.0",
76
83
  id: msg.id,
77
84
  result: {
78
85
  protocolVersion,
79
86
  capabilities: { tools: {} },
80
87
  serverInfo: info,
81
- }
88
+ },
82
89
  };
83
90
  res.write(`event: message\n`);
84
91
  res.write(`data: ${JSON.stringify(response)}\n\n`);
@@ -88,48 +95,48 @@ async function handleStreamableReinitiate(req, res, transport, serverBackendFact
88
95
  return await transport.handleRequest(req, res, body);
89
96
  }
90
97
  async function handleSSE(serverBackendFactory, req, res, url, sessions) {
91
- if (req.method === 'POST') {
92
- const sessionId = url.searchParams.get('sessionId');
98
+ if (req.method === "POST") {
99
+ const sessionId = url.searchParams.get("sessionId");
93
100
  if (!sessionId) {
94
101
  res.statusCode = 400;
95
- return res.end('Missing sessionId');
102
+ return res.end("Missing sessionId");
96
103
  }
97
104
  const transport = sessions.get(sessionId);
98
105
  if (!transport) {
99
106
  res.statusCode = 404;
100
- return res.end('Session not found');
107
+ return res.end("Session not found");
101
108
  }
102
109
  return await transport.handlePostMessage(req, res);
103
110
  }
104
- else if (req.method === 'GET') {
105
- const transport = new SSEServerTransport('/sse', res);
111
+ else if (req.method === "GET") {
112
+ const transport = new SSEServerTransport("/sse", res);
106
113
  sessions.set(transport.sessionId, transport);
107
114
  testDebug(`create SSE session: ${transport.sessionId}`);
108
115
  await mcpServer.connect(serverBackendFactory, transport, false);
109
- res.on('close', () => {
116
+ res.on("close", () => {
110
117
  testDebug(`delete SSE session: ${transport.sessionId}`);
111
118
  sessions.delete(transport.sessionId);
112
119
  });
113
120
  return;
114
121
  }
115
122
  res.statusCode = 405;
116
- res.end('Method not allowed');
123
+ res.end("Method not allowed");
117
124
  }
118
125
  // Streamable transport: 'initialize' handling per MCP Lifecycle (Initialization) https://modelcontextprotocol.io/specification/draft/basic/lifecycle#initialization
119
126
  async function handleStreamable(serverBackendFactory, req, res, sessions, serverInfos) {
120
- const sessionId = req.headers['mcp-session-id'];
127
+ const sessionId = req.headers["mcp-session-id"];
121
128
  if (sessionId) {
122
129
  const transport = sessions.get(sessionId);
123
130
  if (!transport) {
124
131
  res.statusCode = 404;
125
- res.end('Session not found');
132
+ res.end("Session not found");
126
133
  return;
127
134
  }
128
- if (req.method === 'POST')
135
+ if (req.method === "POST")
129
136
  return await handleStreamableReinitiate(req, res, transport, serverBackendFactory, sessionId, serverInfos);
130
137
  return await transport.handleRequest(req, res);
131
138
  }
132
- if (req.method === 'POST') {
139
+ if (req.method === "POST") {
133
140
  const transport = new StreamableHTTPServerTransport({
134
141
  sessionIdGenerator: () => crypto.randomUUID(),
135
142
  onsessioninitialized: async (sessionId) => {
@@ -137,7 +144,7 @@ async function handleStreamable(serverBackendFactory, req, res, sessions, server
137
144
  const serverInfo = await mcpServer.connect(serverBackendFactory, transport, false);
138
145
  sessions.set(sessionId, transport);
139
146
  serverInfos.set(sessionId, serverInfo);
140
- }
147
+ },
141
148
  });
142
149
  transport.onclose = () => {
143
150
  if (!transport.sessionId)
@@ -150,15 +157,15 @@ async function handleStreamable(serverBackendFactory, req, res, sessions, server
150
157
  return;
151
158
  }
152
159
  res.statusCode = 400;
153
- res.end('Invalid request');
160
+ res.end("Invalid request");
154
161
  }
155
162
  function startHttpTransport(httpServer, serverBackendFactory) {
156
163
  const sseSessions = new Map();
157
164
  const streamableSessions = new Map();
158
165
  const streamableServerInfos = new Map();
159
- httpServer.on('request', async (req, res) => {
166
+ httpServer.on("request", async (req, res) => {
160
167
  const url = new URL(`http://localhost${req.url}`);
161
- if (url.pathname.startsWith('/sse'))
168
+ if (url.pathname.startsWith("/sse"))
162
169
  await handleSSE(serverBackendFactory, req, res, url, sseSessions);
163
170
  else
164
171
  await handleStreamable(serverBackendFactory, req, res, streamableSessions, streamableServerInfos);
@@ -166,16 +173,16 @@ function startHttpTransport(httpServer, serverBackendFactory) {
166
173
  const url = httpAddressToString(httpServer.address());
167
174
  const message = [
168
175
  `Listening on ${url}`,
169
- 'Put this in your client config:',
176
+ "Put this in your client config:",
170
177
  JSON.stringify({
171
- 'mcpServers': {
172
- 'playwright': {
173
- 'url': `${url}/mcp`
174
- }
175
- }
178
+ mcpServers: {
179
+ playwright: {
180
+ url: `${url}/mcp`,
181
+ },
182
+ },
176
183
  }, undefined, 2),
177
- 'For legacy SSE transport support, you can use the /sse endpoint instead.',
178
- ].join('\n');
184
+ "For legacy SSE transport support, you can use the /sse endpoint instead.",
185
+ ].join("\n");
179
186
  // eslint-disable-next-line no-console
180
187
  console.error(message);
181
188
  }