@wordbricks/playwright-mcp 0.1.25 → 0.1.26

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 (82) hide show
  1. package/lib/browserContextFactory.js +399 -0
  2. package/lib/browserServerBackend.js +86 -0
  3. package/lib/config.js +300 -0
  4. package/lib/context.js +311 -0
  5. package/lib/extension/cdpRelay.js +352 -0
  6. package/lib/extension/extensionContextFactory.js +56 -0
  7. package/lib/frameworkPatterns.js +35 -0
  8. package/lib/hooks/antiBotDetectionHook.js +178 -0
  9. package/lib/hooks/core.js +145 -0
  10. package/lib/hooks/eventConsumer.js +52 -0
  11. package/lib/hooks/events.js +42 -0
  12. package/lib/hooks/formatToolCallEvent.js +12 -0
  13. package/lib/hooks/frameworkStateHook.js +182 -0
  14. package/lib/hooks/grouping.js +72 -0
  15. package/lib/hooks/jsonLdDetectionHook.js +182 -0
  16. package/lib/hooks/networkFilters.js +82 -0
  17. package/lib/hooks/networkSetup.js +61 -0
  18. package/lib/hooks/networkTrackingHook.js +67 -0
  19. package/lib/hooks/pageHeightHook.js +75 -0
  20. package/lib/hooks/registry.js +41 -0
  21. package/lib/hooks/requireTabHook.js +26 -0
  22. package/lib/hooks/schema.js +89 -0
  23. package/lib/hooks/waitHook.js +33 -0
  24. package/lib/index.js +41 -0
  25. package/lib/mcp/inProcessTransport.js +71 -0
  26. package/lib/mcp/proxyBackend.js +130 -0
  27. package/lib/mcp/server.js +91 -0
  28. package/lib/mcp/tool.js +44 -0
  29. package/lib/mcp/transport.js +188 -0
  30. package/lib/playwrightTransformer.js +520 -0
  31. package/lib/program.js +112 -0
  32. package/lib/response.js +192 -0
  33. package/lib/sessionLog.js +123 -0
  34. package/lib/tab.js +251 -0
  35. package/lib/tools/common.js +55 -0
  36. package/lib/tools/console.js +33 -0
  37. package/lib/tools/dialogs.js +50 -0
  38. package/lib/tools/evaluate.js +62 -0
  39. package/lib/tools/extractFrameworkState.js +225 -0
  40. package/lib/tools/files.js +48 -0
  41. package/lib/tools/form.js +66 -0
  42. package/lib/tools/getSnapshot.js +36 -0
  43. package/lib/tools/getVisibleHtml.js +68 -0
  44. package/lib/tools/install.js +51 -0
  45. package/lib/tools/keyboard.js +83 -0
  46. package/lib/tools/mouse.js +97 -0
  47. package/lib/tools/navigate.js +66 -0
  48. package/lib/tools/network.js +121 -0
  49. package/lib/tools/networkDetail.js +238 -0
  50. package/lib/tools/networkSearch/bodySearch.js +161 -0
  51. package/lib/tools/networkSearch/grouping.js +37 -0
  52. package/lib/tools/networkSearch/helpers.js +32 -0
  53. package/lib/tools/networkSearch/searchHtml.js +76 -0
  54. package/lib/tools/networkSearch/types.js +1 -0
  55. package/lib/tools/networkSearch/urlSearch.js +124 -0
  56. package/lib/tools/networkSearch.js +278 -0
  57. package/lib/tools/pdf.js +41 -0
  58. package/lib/tools/repl.js +414 -0
  59. package/lib/tools/screenshot.js +103 -0
  60. package/lib/tools/scroll.js +131 -0
  61. package/lib/tools/snapshot.js +161 -0
  62. package/lib/tools/tabs.js +62 -0
  63. package/lib/tools/tool.js +35 -0
  64. package/lib/tools/utils.js +78 -0
  65. package/lib/tools/wait.js +60 -0
  66. package/lib/tools.js +68 -0
  67. package/lib/utils/adBlockFilter.js +90 -0
  68. package/lib/utils/codegen.js +55 -0
  69. package/lib/utils/extensionPath.js +10 -0
  70. package/lib/utils/fileUtils.js +40 -0
  71. package/lib/utils/graphql.js +269 -0
  72. package/lib/utils/guid.js +22 -0
  73. package/lib/utils/httpServer.js +39 -0
  74. package/lib/utils/log.js +21 -0
  75. package/lib/utils/manualPromise.js +111 -0
  76. package/lib/utils/networkFormat.js +14 -0
  77. package/lib/utils/package.js +20 -0
  78. package/lib/utils/result.js +2 -0
  79. package/lib/utils/sanitizeHtml.js +130 -0
  80. package/lib/utils/truncate.js +103 -0
  81. package/lib/utils/withTimeout.js +7 -0
  82. package/package.json +11 -1
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
17
+ import { ListRootsRequestSchema, PingRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
18
+ import { z } from "zod";
19
+ import { zodToJsonSchema } from "zod-to-json-schema";
20
+ import { logUnhandledError } from "../utils/log.js";
21
+ import { packageJSON } from "../utils/package.js";
22
+ export class ProxyBackend {
23
+ name = "Playwright MCP Client Switcher";
24
+ version = packageJSON.version;
25
+ _mcpProviders;
26
+ _currentClient;
27
+ _contextSwitchTool;
28
+ _roots = [];
29
+ constructor(mcpProviders) {
30
+ this._mcpProviders = mcpProviders;
31
+ this._contextSwitchTool = this._defineContextSwitchTool();
32
+ }
33
+ async initialize(server) {
34
+ if (this._currentClient)
35
+ return;
36
+ const version = server.getClientVersion();
37
+ const capabilities = server.getClientCapabilities();
38
+ if (capabilities?.roots &&
39
+ version &&
40
+ clientsWithRoots.includes(version.name)) {
41
+ const { roots } = await server.listRoots();
42
+ this._roots = roots;
43
+ }
44
+ await this._setCurrentClient(this._mcpProviders[0]);
45
+ }
46
+ async listTools() {
47
+ const response = await this._currentClient.listTools();
48
+ if (this._mcpProviders.length === 1)
49
+ return response.tools;
50
+ return [...response.tools, this._contextSwitchTool];
51
+ }
52
+ async callTool(name, args) {
53
+ if (name === this._contextSwitchTool.name)
54
+ return this._callContextSwitchTool(args);
55
+ return (await this._currentClient.callTool({
56
+ name,
57
+ arguments: args,
58
+ }));
59
+ }
60
+ serverClosed() {
61
+ void this._currentClient?.close().catch(logUnhandledError);
62
+ this._currentClient = undefined;
63
+ }
64
+ async _callContextSwitchTool(params) {
65
+ try {
66
+ const factory = this._mcpProviders.find((factory) => factory.name === params.name);
67
+ if (!factory)
68
+ throw new Error("Unknown connection method: " + params.name);
69
+ await this._setCurrentClient(factory);
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: "### Result\nSuccessfully changed connection method.\n",
75
+ },
76
+ ],
77
+ };
78
+ }
79
+ catch (error) {
80
+ return {
81
+ content: [{ type: "text", text: `### Result\nError: ${error}\n` }],
82
+ isError: true,
83
+ };
84
+ }
85
+ }
86
+ _defineContextSwitchTool() {
87
+ return {
88
+ name: "browser_connect",
89
+ description: [
90
+ "Connect to a browser using one of the available methods:",
91
+ ...this._mcpProviders.map((factory) => `- "${factory.name}": ${factory.description}`),
92
+ ].join("\n"),
93
+ inputSchema: zodToJsonSchema(z.object({
94
+ name: z
95
+ .enum(this._mcpProviders.map((factory) => factory.name))
96
+ .default(this._mcpProviders[0].name)
97
+ .describe("The method to use to connect to the browser"),
98
+ }), { strictUnions: true }),
99
+ annotations: {
100
+ title: "Connect to a browser context",
101
+ readOnlyHint: true,
102
+ openWorldHint: false,
103
+ },
104
+ };
105
+ }
106
+ async _setCurrentClient(factory) {
107
+ await this._currentClient?.close();
108
+ this._currentClient = undefined;
109
+ const client = new Client({
110
+ name: "Playwright MCP Proxy",
111
+ version: packageJSON.version,
112
+ });
113
+ client.registerCapabilities({
114
+ roots: {
115
+ listChanged: true,
116
+ },
117
+ });
118
+ client.setRequestHandler(ListRootsRequestSchema, () => ({
119
+ roots: this._roots,
120
+ }));
121
+ client.setRequestHandler(PingRequestSchema, () => ({}));
122
+ const transport = await factory.connect();
123
+ await client.connect(transport);
124
+ this._currentClient = client;
125
+ }
126
+ }
127
+ const clientsWithRoots = [
128
+ "Visual Studio Code",
129
+ "Visual Studio Code - Insiders",
130
+ ];
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
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
+ export async function connect(serverBackendFactory, transport, runHeartbeat) {
24
+ const backend = serverBackendFactory();
25
+ const server = createServer(backend, runHeartbeat);
26
+ await server.connect(transport);
27
+ return { name: backend.name, version: backend.version };
28
+ }
29
+ export function createServer(backend, runHeartbeat) {
30
+ const initializedPromise = new ManualPromise();
31
+ const server = new Server({ name: backend.name, version: backend.version }, {
32
+ capabilities: {
33
+ tools: {},
34
+ },
35
+ });
36
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
37
+ serverDebug("listTools");
38
+ await initializedPromise;
39
+ const tools = await backend.listTools();
40
+ return { tools };
41
+ });
42
+ let heartbeatRunning = false;
43
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
44
+ serverDebug("callTool", request);
45
+ await initializedPromise;
46
+ if (runHeartbeat && !heartbeatRunning) {
47
+ heartbeatRunning = true;
48
+ startHeartbeat(server);
49
+ }
50
+ try {
51
+ return await backend.callTool(request.params.name, request.params.arguments || {});
52
+ }
53
+ catch (error) {
54
+ return {
55
+ content: [{ type: "text", text: "### Result\n" + String(error) }],
56
+ isError: true,
57
+ };
58
+ }
59
+ });
60
+ addServerListener(server, "initialized", () => {
61
+ backend
62
+ .initialize?.(server)
63
+ .then(() => initializedPromise.resolve())
64
+ .catch(logUnhandledError);
65
+ });
66
+ addServerListener(server, "close", () => backend.serverClosed?.());
67
+ return server;
68
+ }
69
+ const startHeartbeat = (server) => {
70
+ const beat = () => {
71
+ serverDebug("Health check...");
72
+ Promise.race([
73
+ server.ping(),
74
+ new Promise((_, reject) => setTimeout(() => reject(new Error("ping timeout")), ms("5s"))),
75
+ ])
76
+ .then(() => {
77
+ setTimeout(beat, ms("3s"));
78
+ })
79
+ .catch(() => {
80
+ void server.close();
81
+ });
82
+ };
83
+ beat();
84
+ };
85
+ function addServerListener(server, event, listener) {
86
+ const oldListener = server[`on${event}`];
87
+ server[`on${event}`] = () => {
88
+ oldListener?.();
89
+ listener();
90
+ };
91
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { z } from "zod";
17
+ import { zodToJsonSchema } from "zod-to-json-schema";
18
+ const typesWithIntent = ["action", "assertion", "input"];
19
+ export function toMcpTool(tool, options) {
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";
28
+ return {
29
+ name: tool.name,
30
+ description: tool.description,
31
+ inputSchema: zodToJsonSchema(inputSchema, {
32
+ strictUnions: true,
33
+ }),
34
+ annotations: {
35
+ title: tool.title,
36
+ readOnlyHint: readOnly,
37
+ destructiveHint: !readOnly,
38
+ openWorldHint: true,
39
+ },
40
+ };
41
+ }
42
+ export function defineToolSchema(tool) {
43
+ return tool;
44
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
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
+ // @see node_modules/@modelcontextprotocol/sdk/dist/esm/server/streamableHttp.js
28
+ const MAXIMUM_MESSAGE_SIZE = "4mb";
29
+ export async function start(serverBackendFactory, options) {
30
+ if (options.port !== undefined) {
31
+ const httpServer = await startHttpServer(options);
32
+ startHttpTransport(httpServer, serverBackendFactory);
33
+ }
34
+ else {
35
+ await startStdioTransport(serverBackendFactory);
36
+ }
37
+ }
38
+ async function startStdioTransport(serverBackendFactory) {
39
+ await mcpServer.connect(serverBackendFactory, new StdioServerTransport(), false);
40
+ }
41
+ const testDebug = debug("pw:mcp:test");
42
+ async function handleStreamableReinitiate(req, res, transport, serverBackendFactory, sessionId, serverInfos) {
43
+ const ct = req.headers["content-type"];
44
+ if (!ct || !ct.includes("application/json")) {
45
+ res.writeHead(415).end(JSON.stringify({
46
+ jsonrpc: "2.0",
47
+ error: {
48
+ code: -32000,
49
+ message: "Unsupported Media Type: Content-Type must be application/json",
50
+ },
51
+ id: null,
52
+ }));
53
+ return;
54
+ }
55
+ const parsedCt = contentType.parse(ct);
56
+ const encoding = parsedCt.parameters.charset;
57
+ const body = await pipe(getRawBody(req, {
58
+ limit: MAXIMUM_MESSAGE_SIZE,
59
+ encoding,
60
+ }), (raw) => raw.toString(), JSON.parse);
61
+ const msg = Array.isArray(body)
62
+ ? body.length === 1
63
+ ? body[0]
64
+ : undefined
65
+ : body;
66
+ if (body && isJSONRPCRequest(msg) && isInitializeRequest(msg)) {
67
+ const requestedVersion = msg.params.protocolVersion;
68
+ const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
69
+ ? requestedVersion
70
+ : LATEST_PROTOCOL_VERSION;
71
+ const headers = {
72
+ "Content-Type": "text/event-stream",
73
+ "Mcp-Session-Id": transport.sessionId,
74
+ };
75
+ res.writeHead(200, headers);
76
+ const info = serverInfos.get(sessionId) ||
77
+ (() => {
78
+ const backend = serverBackendFactory();
79
+ return { name: backend.name, version: backend.version };
80
+ })();
81
+ const response = {
82
+ jsonrpc: "2.0",
83
+ id: msg.id,
84
+ result: {
85
+ protocolVersion,
86
+ capabilities: { tools: {} },
87
+ serverInfo: info,
88
+ },
89
+ };
90
+ res.write(`event: message\n`);
91
+ res.write(`data: ${JSON.stringify(response)}\n\n`);
92
+ res.end();
93
+ return;
94
+ }
95
+ return await transport.handleRequest(req, res, body);
96
+ }
97
+ async function handleSSE(serverBackendFactory, req, res, url, sessions) {
98
+ if (req.method === "POST") {
99
+ const sessionId = url.searchParams.get("sessionId");
100
+ if (!sessionId) {
101
+ res.statusCode = 400;
102
+ return res.end("Missing sessionId");
103
+ }
104
+ const transport = sessions.get(sessionId);
105
+ if (!transport) {
106
+ res.statusCode = 404;
107
+ return res.end("Session not found");
108
+ }
109
+ return await transport.handlePostMessage(req, res);
110
+ }
111
+ else if (req.method === "GET") {
112
+ const transport = new SSEServerTransport("/sse", res);
113
+ sessions.set(transport.sessionId, transport);
114
+ testDebug(`create SSE session: ${transport.sessionId}`);
115
+ await mcpServer.connect(serverBackendFactory, transport, false);
116
+ res.on("close", () => {
117
+ testDebug(`delete SSE session: ${transport.sessionId}`);
118
+ sessions.delete(transport.sessionId);
119
+ });
120
+ return;
121
+ }
122
+ res.statusCode = 405;
123
+ res.end("Method not allowed");
124
+ }
125
+ // Streamable transport: 'initialize' handling per MCP Lifecycle (Initialization) https://modelcontextprotocol.io/specification/draft/basic/lifecycle#initialization
126
+ async function handleStreamable(serverBackendFactory, req, res, sessions, serverInfos) {
127
+ const sessionId = req.headers["mcp-session-id"];
128
+ if (sessionId) {
129
+ const transport = sessions.get(sessionId);
130
+ if (!transport) {
131
+ res.statusCode = 404;
132
+ res.end("Session not found");
133
+ return;
134
+ }
135
+ if (req.method === "POST")
136
+ return await handleStreamableReinitiate(req, res, transport, serverBackendFactory, sessionId, serverInfos);
137
+ return await transport.handleRequest(req, res);
138
+ }
139
+ if (req.method === "POST") {
140
+ const transport = new StreamableHTTPServerTransport({
141
+ sessionIdGenerator: () => crypto.randomUUID(),
142
+ onsessioninitialized: async (sessionId) => {
143
+ testDebug(`create http session: ${transport.sessionId}`);
144
+ const serverInfo = await mcpServer.connect(serverBackendFactory, transport, false);
145
+ sessions.set(sessionId, transport);
146
+ serverInfos.set(sessionId, serverInfo);
147
+ },
148
+ });
149
+ transport.onclose = () => {
150
+ if (!transport.sessionId)
151
+ return;
152
+ sessions.delete(transport.sessionId);
153
+ serverInfos.delete(transport.sessionId);
154
+ testDebug(`delete http session: ${transport.sessionId}`);
155
+ };
156
+ await transport.handleRequest(req, res);
157
+ return;
158
+ }
159
+ res.statusCode = 400;
160
+ res.end("Invalid request");
161
+ }
162
+ function startHttpTransport(httpServer, serverBackendFactory) {
163
+ const sseSessions = new Map();
164
+ const streamableSessions = new Map();
165
+ const streamableServerInfos = new Map();
166
+ httpServer.on("request", async (req, res) => {
167
+ const url = new URL(`http://localhost${req.url}`);
168
+ if (url.pathname.startsWith("/sse"))
169
+ await handleSSE(serverBackendFactory, req, res, url, sseSessions);
170
+ else
171
+ await handleStreamable(serverBackendFactory, req, res, streamableSessions, streamableServerInfos);
172
+ });
173
+ const url = httpAddressToString(httpServer.address());
174
+ const message = [
175
+ `Listening on ${url}`,
176
+ "Put this in your client config:",
177
+ JSON.stringify({
178
+ mcpServers: {
179
+ playwright: {
180
+ url: `${url}/mcp`,
181
+ },
182
+ },
183
+ }, undefined, 2),
184
+ "For legacy SSE transport support, you can use the /sse endpoint instead.",
185
+ ].join("\n");
186
+ // eslint-disable-next-line no-console
187
+ console.error(message);
188
+ }