@wordbricks/playwright-mcp 0.1.19 → 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 (88) hide show
  1. package/README.md +54 -44
  2. package/cli-wrapper.js +15 -14
  3. package/cli.js +1 -1
  4. package/config.d.ts +11 -6
  5. package/index.d.ts +7 -5
  6. package/index.js +1 -1
  7. package/lib/browserContextFactory.js +131 -58
  8. package/lib/browserServerBackend.js +14 -12
  9. package/lib/config.js +60 -46
  10. package/lib/context.js +41 -39
  11. package/lib/extension/cdpRelay.js +67 -61
  12. package/lib/extension/extensionContextFactory.js +10 -10
  13. package/lib/frameworkPatterns.js +21 -21
  14. package/lib/hooks/antiBotDetectionHook.js +178 -0
  15. package/lib/hooks/core.js +11 -10
  16. package/lib/hooks/eventConsumer.js +29 -16
  17. package/lib/hooks/events.js +3 -3
  18. package/lib/hooks/formatToolCallEvent.js +3 -7
  19. package/lib/hooks/frameworkStateHook.js +40 -40
  20. package/lib/hooks/grouping.js +3 -3
  21. package/lib/hooks/jsonLdDetectionHook.js +44 -37
  22. package/lib/hooks/networkFilters.js +24 -15
  23. package/lib/hooks/networkSetup.js +11 -6
  24. package/lib/hooks/networkTrackingHook.js +31 -19
  25. package/lib/hooks/pageHeightHook.js +9 -9
  26. package/lib/hooks/registry.js +18 -16
  27. package/lib/hooks/requireTabHook.js +3 -3
  28. package/lib/hooks/schema.js +44 -32
  29. package/lib/hooks/waitHook.js +7 -7
  30. package/lib/index.js +12 -10
  31. package/lib/mcp/inProcessTransport.js +3 -4
  32. package/lib/mcp/proxyBackend.js +43 -28
  33. package/lib/mcp/server.js +24 -19
  34. package/lib/mcp/tool.js +14 -8
  35. package/lib/mcp/transport.js +60 -53
  36. package/lib/playwrightTransformer.js +129 -106
  37. package/lib/program.js +54 -52
  38. package/lib/response.js +36 -30
  39. package/lib/sessionLog.js +19 -17
  40. package/lib/tab.js +41 -39
  41. package/lib/tools/common.js +19 -19
  42. package/lib/tools/console.js +11 -11
  43. package/lib/tools/dialogs.js +18 -15
  44. package/lib/tools/evaluate.js +26 -17
  45. package/lib/tools/extractFrameworkState.js +48 -37
  46. package/lib/tools/files.js +17 -14
  47. package/lib/tools/form.js +32 -23
  48. package/lib/tools/getSnapshot.js +14 -15
  49. package/lib/tools/getVisibleHtml.js +33 -17
  50. package/lib/tools/install.js +20 -20
  51. package/lib/tools/keyboard.js +29 -24
  52. package/lib/tools/mouse.js +29 -31
  53. package/lib/tools/navigate.js +19 -23
  54. package/lib/tools/network.js +12 -14
  55. package/lib/tools/networkDetail.js +68 -61
  56. package/lib/tools/networkSearch/bodySearch.js +46 -32
  57. package/lib/tools/networkSearch/grouping.js +15 -6
  58. package/lib/tools/networkSearch/helpers.js +4 -4
  59. package/lib/tools/networkSearch/searchHtml.js +25 -16
  60. package/lib/tools/networkSearch/urlSearch.js +56 -14
  61. package/lib/tools/networkSearch.js +65 -35
  62. package/lib/tools/pdf.js +13 -12
  63. package/lib/tools/repl.js +66 -54
  64. package/lib/tools/screenshot.js +57 -33
  65. package/lib/tools/scroll.js +29 -24
  66. package/lib/tools/snapshot.js +66 -49
  67. package/lib/tools/tabs.js +22 -19
  68. package/lib/tools/tool.js +5 -3
  69. package/lib/tools/utils.js +17 -13
  70. package/lib/tools/wait.js +24 -19
  71. package/lib/tools.js +21 -20
  72. package/lib/utils/adBlockFilter.js +29 -26
  73. package/lib/utils/codegen.js +20 -16
  74. package/lib/utils/extensionPath.js +4 -4
  75. package/lib/utils/fileUtils.js +17 -13
  76. package/lib/utils/graphql.js +69 -58
  77. package/lib/utils/guid.js +3 -3
  78. package/lib/utils/httpServer.js +9 -9
  79. package/lib/utils/log.js +3 -3
  80. package/lib/utils/manualPromise.js +7 -7
  81. package/lib/utils/networkFormat.js +7 -5
  82. package/lib/utils/package.js +4 -4
  83. package/lib/utils/sanitizeHtml.js +66 -34
  84. package/lib/utils/truncate.js +25 -25
  85. package/lib/utils/withTimeout.js +1 -1
  86. package/package.json +34 -57
  87. package/src/index.ts +27 -17
  88. package/LICENSE +0 -202
@@ -1,20 +1,20 @@
1
- import { toArray, pipe, filter } from '@fxts/core';
2
- import { parseGraphQLRequestFromHttp } from '../utils/graphql.js';
3
- import { formatNetworkSummaryLine } from '../utils/networkFormat.js';
4
- import { defineGroupingRule } from './grouping.js';
5
- import { Ok } from '../utils/result.js';
6
- import { hookNameSchema } from './schema.js';
7
- import { normalizeUrlForGrouping } from './networkFilters.js';
8
- import { getEventStore, isEventType } from './events.js';
1
+ import { filter, pipe, toArray } from "@fxts/core";
2
+ import { parseGraphQLRequestFromHttp } from "../utils/graphql.js";
3
+ import { formatNetworkSummaryLine } from "../utils/networkFormat.js";
4
+ import { Ok } from "../utils/result.js";
5
+ import { getEventStore, isEventType } from "./events.js";
6
+ import { defineGroupingRule } from "./grouping.js";
7
+ import { normalizeUrlForGrouping } from "./networkFilters.js";
8
+ import { hookNameSchema } from "./schema.js";
9
9
  const networkTrackingPreHook = {
10
- name: hookNameSchema.enum['network-tracking-pre'],
10
+ name: hookNameSchema.enum["network-tracking-pre"],
11
11
  handler: async (_ctx) => {
12
12
  // Pre-hook now just acts as a marker, event consumption happens elsewhere
13
13
  return Ok(undefined);
14
14
  },
15
15
  };
16
16
  const networkTrackingPostHook = {
17
- name: hookNameSchema.enum['network-tracking-post'],
17
+ name: hookNameSchema.enum["network-tracking-post"],
18
18
  handler: async (_ctx) => {
19
19
  // Post-hook now just acts as a marker, network event collection happens automatically
20
20
  return Ok(undefined);
@@ -25,31 +25,43 @@ export const networkTrackingHooks = {
25
25
  post: networkTrackingPostHook,
26
26
  };
27
27
  export const formatNetworkEvent = (event) => {
28
- const { method, url, status, postData } = event.data;
29
- return formatNetworkSummaryLine({ method, url, status, postData });
28
+ const { method, url, status, postData, setCookies } = event.data;
29
+ const summary = formatNetworkSummaryLine({ method, url, status, postData });
30
+ if (!setCookies || setCookies.length === 0)
31
+ return summary;
32
+ const names = setCookies
33
+ .map((cookie) => {
34
+ const firstPart = cookie.split(";", 1)[0];
35
+ const [name] = firstPart.split("=", 1);
36
+ return name?.trim();
37
+ })
38
+ .filter((name) => !!name);
39
+ if (!names.length)
40
+ return summary;
41
+ return `${summary} | Set-Cookie keys: ${names.join(", ")}`;
30
42
  };
31
43
  const computeNetworkGroupKey = (event) => {
32
- const method = (event.data.method || '').toUpperCase();
44
+ const method = (event.data.method || "").toUpperCase();
33
45
  const baseUrl = normalizeUrlForGrouping(event.data.url);
34
46
  const gql = parseGraphQLRequestFromHttp(event.data.method, event.data.url, {}, event.data.postData);
35
47
  if (gql) {
36
- const type = gql.operationType === 'unknown' ? 'operation' : gql.operationType;
48
+ const type = gql.operationType === "unknown" ? "operation" : gql.operationType;
37
49
  const op = gql.operationName ? `${type} ${gql.operationName}` : type;
38
50
  return `${method} ${baseUrl} [GraphQL: ${op}]`;
39
51
  }
40
52
  return `${method} ${baseUrl}`;
41
53
  };
42
54
  export const networkGroupingRule = defineGroupingRule({
43
- match: (e) => e.type === 'network-request',
44
- keyOf: e => computeNetworkGroupKey(e),
55
+ match: (e) => e.type === "network-request",
56
+ keyOf: (e) => computeNetworkGroupKey(e),
45
57
  summaryOf: (first, run) => {
46
58
  const key = computeNetworkGroupKey(first);
47
59
  const count = run.length;
48
60
  const firstStatus = first.data.status;
49
- const allSameStatus = run.every(e => e.data.status === firstStatus);
61
+ const allSameStatus = run.every((e) => e.data.status === firstStatus);
50
62
  return allSameStatus
51
63
  ? `${key} → ${firstStatus} (x${count})`
52
64
  : `${key} (x${count})`;
53
- }
65
+ },
54
66
  });
55
- export const listNetworkEvents = (context) => pipe(context, getEventStore, store => store.events.values(), filter(isEventType('network-request')), toArray);
67
+ export const listNetworkEvents = (context) => pipe(context, getEventStore, (store) => store.events.values(), filter(isEventType("network-request")), toArray);
@@ -1,6 +1,6 @@
1
- import { Ok, Err } from '../utils/result.js';
2
- import { hookNameSchema } from './schema.js';
3
- import { trackEvent } from './events.js';
1
+ import { Err, Ok } from "../utils/result.js";
2
+ import { trackEvent } from "./events.js";
3
+ import { hookNameSchema } from "./schema.js";
4
4
  const pageHeightStateMap = new WeakMap();
5
5
  const capturePageHeight = async (ctx) => {
6
6
  const tab = ctx.tab || ctx.context.currentTab();
@@ -9,7 +9,7 @@ const capturePageHeight = async (ctx) => {
9
9
  try {
10
10
  return await tab.page.evaluate(() => ({
11
11
  pageHeight: document.documentElement.scrollHeight,
12
- scrollY: window.scrollY
12
+ scrollY: window.scrollY,
13
13
  }));
14
14
  }
15
15
  catch {
@@ -17,7 +17,7 @@ const capturePageHeight = async (ctx) => {
17
17
  }
18
18
  };
19
19
  const pageHeightPreHook = {
20
- name: hookNameSchema.enum['page-height-pre'],
20
+ name: hookNameSchema.enum["page-height-pre"],
21
21
  handler: async (ctx) => {
22
22
  try {
23
23
  const initialState = await capturePageHeight(ctx);
@@ -33,7 +33,7 @@ const pageHeightPreHook = {
33
33
  },
34
34
  };
35
35
  const pageHeightPostHook = {
36
- name: hookNameSchema.enum['page-height-post'],
36
+ name: hookNameSchema.enum["page-height-post"],
37
37
  handler: async (ctx) => {
38
38
  try {
39
39
  const initialState = pageHeightStateMap.get(ctx.context);
@@ -44,9 +44,9 @@ const pageHeightPostHook = {
44
44
  return Ok(undefined);
45
45
  const heightChange = finalState.pageHeight - initialState.pageHeight;
46
46
  const tab = ctx.tab || ctx.context.currentTab();
47
- const url = tab ? tab.page.url() : 'unknown';
47
+ const url = tab ? tab.page.url() : "unknown";
48
48
  trackEvent(ctx.context, {
49
- type: 'page-height-change',
49
+ type: "page-height-change",
50
50
  data: {
51
51
  previousHeight: initialState.pageHeight,
52
52
  currentHeight: finalState.pageHeight,
@@ -71,5 +71,5 @@ export const formatPageHeightEvent = (event) => {
71
71
  const { previousHeight, currentHeight, heightChange } = event.data;
72
72
  if (heightChange === 0)
73
73
  return `Page height unchanged.`;
74
- return `Page height changed: ${previousHeight}px → ${currentHeight}px (${heightChange > 0 ? '+' : ''}${heightChange}px)`;
74
+ return `Page height changed: ${previousHeight}px → ${currentHeight}px (${heightChange > 0 ? "+" : ""}${heightChange}px)`;
75
75
  };
@@ -1,28 +1,30 @@
1
- import { pipe, reduce, values } from '@fxts/core';
2
- import { createHookRegistry, setToolHooks } from './core.js';
3
- import { networkTrackingHooks } from './networkTrackingHook.js';
4
- import { pageHeightHooks } from './pageHeightHook.js';
5
- import { waitHooks } from './waitHook.js';
6
- import { frameworkStateHooks } from './frameworkStateHook.js';
7
- import { jsonLdDetectionHooks } from './jsonLdDetectionHook.js';
8
- import { toolNameSchema } from './schema.js';
9
- import { requireTabHooks } from './requireTabHook.js';
10
- import { registerGroupingRule } from './grouping.js';
11
- import { networkGroupingRule } from './networkTrackingHook.js';
1
+ import { pipe, reduce, values } from "@fxts/core";
2
+ import { antiBotDetectionHooks } from "./antiBotDetectionHook.js";
3
+ import { createHookRegistry, setToolHooks } from "./core.js";
4
+ import { frameworkStateHooks } from "./frameworkStateHook.js";
5
+ import { registerGroupingRule } from "./grouping.js";
6
+ import { jsonLdDetectionHooks } from "./jsonLdDetectionHook.js";
7
+ import { networkGroupingRule, networkTrackingHooks, } from "./networkTrackingHook.js";
8
+ import { pageHeightHooks } from "./pageHeightHook.js";
9
+ import { requireTabHooks } from "./requireTabHook.js";
10
+ import { toolNameSchema } from "./schema.js";
11
+ import { waitHooks } from "./waitHook.js";
12
12
  const COMMON_HOOKS = {
13
13
  preHooks: [
14
14
  requireTabHooks.pre,
15
15
  networkTrackingHooks.pre,
16
+ antiBotDetectionHooks.pre,
16
17
  pageHeightHooks.pre,
17
18
  frameworkStateHooks.pre,
18
- jsonLdDetectionHooks.pre
19
+ jsonLdDetectionHooks.pre,
19
20
  ],
20
21
  postHooks: [
22
+ networkTrackingHooks.post,
23
+ antiBotDetectionHooks.post,
21
24
  frameworkStateHooks.post,
22
25
  jsonLdDetectionHooks.post,
23
- networkTrackingHooks.post,
24
26
  pageHeightHooks.post,
25
- waitHooks.post
27
+ waitHooks.post,
26
28
  ],
27
29
  };
28
30
  const toolHooksConfig = [
@@ -34,6 +36,6 @@ const toolHooksConfig = [
34
36
  // },
35
37
  ];
36
38
  export const buildHookRegistry = () => {
37
- registerGroupingRule('network-request', networkGroupingRule);
38
- return pipe(createHookRegistry(), registry => reduce((acc, toolName) => setToolHooks(acc, { toolName, ...COMMON_HOOKS }), registry, values(toolNameSchema.enum)), registry => reduce(setToolHooks, registry, toolHooksConfig));
39
+ registerGroupingRule("network-request", networkGroupingRule);
40
+ return pipe(createHookRegistry(), (registry) => reduce((acc, toolName) => setToolHooks(acc, { toolName, ...COMMON_HOOKS }), registry, values(toolNameSchema.enum)), (registry) => reduce(setToolHooks, registry, toolHooksConfig));
39
41
  };
@@ -1,8 +1,8 @@
1
- import { Ok, Err } from '../utils/result.js';
2
- import { hookNameSchema, toolNameSchema } from './schema.js';
1
+ import { Err, Ok } from "../utils/result.js";
2
+ import { hookNameSchema, toolNameSchema } from "./schema.js";
3
3
  const MESSAGE = 'No open tabs. Use the "browser_navigate" tool to navigate to a page first.';
4
4
  const requireTabPreHook = {
5
- name: hookNameSchema.enum['require-tab-pre'],
5
+ name: hookNameSchema.enum["require-tab-pre"],
6
6
  handler: async (ctx) => {
7
7
  try {
8
8
  // Allow navigate tool to create/open a tab
@@ -1,42 +1,45 @@
1
- import { z } from 'zod';
1
+ import { z } from "zod";
2
2
  export const hookNameSchema = z.enum([
3
- 'network-tracking-pre',
4
- 'network-tracking-post',
5
- 'page-height-pre',
6
- 'page-height-post',
7
- 'wait-post',
8
- 'framework-state-pre',
9
- 'framework-state-post',
10
- 'json-ld-detection-pre',
11
- 'json-ld-detection-post',
12
- 'require-tab-pre',
3
+ "network-tracking-pre",
4
+ "network-tracking-post",
5
+ "page-height-pre",
6
+ "page-height-post",
7
+ "wait-post",
8
+ "framework-state-pre",
9
+ "framework-state-post",
10
+ "json-ld-detection-pre",
11
+ "json-ld-detection-post",
12
+ "require-tab-pre",
13
+ "anti-bot-detection-pre",
14
+ "anti-bot-detection-post",
13
15
  ]);
14
16
  // Tool names enum - should match actual tool names in
15
17
  export const toolNameSchema = z.enum([
16
- 'browser_click',
17
- 'browser_extract_framework_state',
18
- 'browser_fill_form',
19
- 'browser_get_snapshot',
18
+ "browser_click",
19
+ "browser_extract_framework_state",
20
+ "browser_fill_form",
21
+ "browser_get_snapshot",
20
22
  // 'browser_get_visible_html',
21
- 'browser_navigate',
23
+ "browser_navigate",
22
24
  // 'browser_navigate_back',
23
25
  // 'browser_navigate_forward',
24
- 'browser_network_detail',
25
- 'browser_network_search',
26
- 'browser_press_key',
27
- 'browser_reload',
28
- 'browser_repl',
29
- 'browser_scroll',
30
- 'browser_type',
31
- 'browser_wait',
26
+ "browser_network_detail",
27
+ "browser_network_search",
28
+ "browser_press_key",
29
+ "browser_reload",
30
+ "browser_repl",
31
+ "browser_scroll",
32
+ "browser_type",
33
+ "browser_wait",
32
34
  ]);
33
35
  export const EventTypeSchema = z.enum([
34
- 'network-request',
35
- 'page-height-change',
36
- 'wait',
37
- 'tool-call',
38
- 'framework-state',
39
- 'json-ld',
36
+ "network-request",
37
+ "page-height-change",
38
+ "wait",
39
+ "tool-call",
40
+ "framework-state",
41
+ "json-ld",
42
+ "anti-bot",
40
43
  ]);
41
44
  export const NetworkRequestEventDataSchema = z.object({
42
45
  method: z.string(),
@@ -44,6 +47,7 @@ export const NetworkRequestEventDataSchema = z.object({
44
47
  status: z.number(),
45
48
  resourceType: z.string(),
46
49
  postData: z.string().optional(),
50
+ setCookies: z.array(z.string()).optional(),
47
51
  responseSize: z.number().optional(),
48
52
  });
49
53
  export const PageHeightChangeEventDataSchema = z.object({
@@ -65,7 +69,7 @@ export const ToolCallEventDataSchema = z.object({
65
69
  export const FrameworkStateEventDataSchema = z.object({
66
70
  state: z.record(z.any()),
67
71
  changes: z.array(z.string()).optional(),
68
- action: z.enum(['detected', 'changed']),
72
+ action: z.enum(["detected", "changed"]),
69
73
  });
70
74
  export const JsonLdEventDataSchema = z.object({
71
75
  state: z.record(z.object({
@@ -73,5 +77,13 @@ export const JsonLdEventDataSchema = z.object({
73
77
  indices: z.array(z.number()),
74
78
  })),
75
79
  changes: z.array(z.string()).optional(),
76
- action: z.enum(['detected', 'changed']),
80
+ action: z.enum(["detected", "changed"]),
81
+ });
82
+ export const AntiBotEventDataSchema = z.object({
83
+ provider: z.enum(["cloudflare-turnstile", "aws-waf"]),
84
+ detectionMethod: z.literal("network-request"),
85
+ url: z.string(),
86
+ status: z.number(),
87
+ action: z.enum(["detected", "resolved", "still-blocked"]),
88
+ waitMs: z.number().optional(),
77
89
  });
@@ -1,11 +1,11 @@
1
- import ms from 'ms';
2
- import { Ok, Err } from '../utils/result.js';
3
- import { trackEvent } from './events.js';
4
- import { hookNameSchema } from './schema.js';
5
- export const WAIT_TIME_STR = '0.5s';
1
+ import ms from "ms";
2
+ import { Err, Ok } from "../utils/result.js";
3
+ import { trackEvent } from "./events.js";
4
+ import { hookNameSchema } from "./schema.js";
5
+ export const WAIT_TIME_STR = "0.5s";
6
6
  export const WAIT_TIME_MS = ms(WAIT_TIME_STR);
7
7
  const waitPostHook = {
8
- name: hookNameSchema.enum['wait-post'],
8
+ name: hookNameSchema.enum["wait-post"],
9
9
  handler: async (ctx) => {
10
10
  try {
11
11
  const tab = ctx.tab || ctx.context.currentTab();
@@ -14,7 +14,7 @@ const waitPostHook = {
14
14
  const startTime = Date.now();
15
15
  await tab.page.waitForTimeout(WAIT_TIME_MS);
16
16
  trackEvent(ctx.context, {
17
- type: 'wait',
17
+ type: "wait",
18
18
  data: {
19
19
  duration: WAIT_TIME_MS,
20
20
  },
package/lib/index.js CHANGED
@@ -13,18 +13,20 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { BrowserServerBackend } from './browserServerBackend.js';
17
- import { resolveConfig } from './config.js';
18
- import { contextFactory } from './browserContextFactory.js';
19
- import * as mcpServer from './mcp/server.js';
20
- export async function createConnection(userConfig = {}, contextGetter) {
21
- const config = await resolveConfig(userConfig);
22
- const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config);
16
+ import { contextFactory } from "./browserContextFactory.js";
17
+ import { BrowserServerBackend } from "./browserServerBackend.js";
18
+ import { resolveConfig } from "./config.js";
19
+ import * as mcpServer from "./mcp/server.js";
20
+ export async function createConnection(userConfig, contextGetter) {
21
+ const config = await resolveConfig(userConfig || {});
22
+ const factory = contextGetter
23
+ ? new SimpleBrowserContextFactory(contextGetter)
24
+ : contextFactory(config);
23
25
  return mcpServer.createServer(new BrowserServerBackend(config, factory), false);
24
26
  }
25
27
  class SimpleBrowserContextFactory {
26
- name = 'custom';
27
- description = 'Connect to a browser using a custom context getter';
28
+ name = "custom";
29
+ description = "Connect to a browser using a custom context getter";
28
30
  _contextGetter;
29
31
  constructor(contextGetter) {
30
32
  this._contextGetter = contextGetter;
@@ -33,7 +35,7 @@ class SimpleBrowserContextFactory {
33
35
  const browserContext = await this._contextGetter();
34
36
  return {
35
37
  browserContext,
36
- close: () => browserContext.close()
38
+ close: () => browserContext.close(),
37
39
  };
38
40
  }
39
41
  }
@@ -23,13 +23,13 @@ export class InProcessTransport {
23
23
  }
24
24
  async start() {
25
25
  if (this._connected)
26
- throw new Error('InprocessTransport already started!');
26
+ throw new Error("InprocessTransport already started!");
27
27
  await this._server.connect(this._serverTransport);
28
28
  this._connected = true;
29
29
  }
30
30
  async send(message, options) {
31
31
  if (!this._connected)
32
- throw new Error('Transport not connected');
32
+ throw new Error("Transport not connected");
33
33
  this._serverTransport._receiveFromClient(message);
34
34
  }
35
35
  async close() {
@@ -53,8 +53,7 @@ class InProcessServerTransport {
53
53
  constructor(clientTransport) {
54
54
  this._clientTransport = clientTransport;
55
55
  }
56
- async start() {
57
- }
56
+ async start() { }
58
57
  async send(message, options) {
59
58
  this._clientTransport._receiveFromServer(message);
60
59
  }
@@ -13,14 +13,14 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { z } from 'zod';
17
- import { zodToJsonSchema } from 'zod-to-json-schema';
18
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
19
- import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
20
- import { logUnhandledError } from '../utils/log.js';
21
- import { packageJSON } from '../utils/package.js';
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
22
  export class ProxyBackend {
23
- name = 'Playwright MCP Client Switcher';
23
+ name = "Playwright MCP Client Switcher";
24
24
  version = packageJSON.version;
25
25
  _mcpProviders;
26
26
  _currentClient;
@@ -35,7 +35,9 @@ export class ProxyBackend {
35
35
  return;
36
36
  const version = server.getClientVersion();
37
37
  const capabilities = server.getClientCapabilities();
38
- if (capabilities?.roots && version && clientsWithRoots.includes(version.name)) {
38
+ if (capabilities?.roots &&
39
+ version &&
40
+ clientsWithRoots.includes(version.name)) {
39
41
  const { roots } = await server.listRoots();
40
42
  this._roots = roots;
41
43
  }
@@ -45,18 +47,15 @@ export class ProxyBackend {
45
47
  const response = await this._currentClient.listTools();
46
48
  if (this._mcpProviders.length === 1)
47
49
  return response.tools;
48
- return [
49
- ...response.tools,
50
- this._contextSwitchTool,
51
- ];
50
+ return [...response.tools, this._contextSwitchTool];
52
51
  }
53
52
  async callTool(name, args) {
54
53
  if (name === this._contextSwitchTool.name)
55
54
  return this._callContextSwitchTool(args);
56
- return await this._currentClient.callTool({
55
+ return (await this._currentClient.callTool({
57
56
  name,
58
57
  arguments: args,
59
- });
58
+ }));
60
59
  }
61
60
  serverClosed() {
62
61
  void this._currentClient?.close().catch(logUnhandledError);
@@ -64,33 +63,41 @@ export class ProxyBackend {
64
63
  }
65
64
  async _callContextSwitchTool(params) {
66
65
  try {
67
- const factory = this._mcpProviders.find(factory => factory.name === params.name);
66
+ const factory = this._mcpProviders.find((factory) => factory.name === params.name);
68
67
  if (!factory)
69
- throw new Error('Unknown connection method: ' + params.name);
68
+ throw new Error("Unknown connection method: " + params.name);
70
69
  await this._setCurrentClient(factory);
71
70
  return {
72
- content: [{ type: 'text', text: '### Result\nSuccessfully changed connection method.\n' }],
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: "### Result\nSuccessfully changed connection method.\n",
75
+ },
76
+ ],
73
77
  };
74
78
  }
75
79
  catch (error) {
76
80
  return {
77
- content: [{ type: 'text', text: `### Result\nError: ${error}\n` }],
81
+ content: [{ type: "text", text: `### Result\nError: ${error}\n` }],
78
82
  isError: true,
79
83
  };
80
84
  }
81
85
  }
82
86
  _defineContextSwitchTool() {
83
87
  return {
84
- name: 'browser_connect',
88
+ name: "browser_connect",
85
89
  description: [
86
- 'Connect to a browser using one of the available methods:',
87
- ...this._mcpProviders.map(factory => `- "${factory.name}": ${factory.description}`),
88
- ].join('\n'),
90
+ "Connect to a browser using one of the available methods:",
91
+ ...this._mcpProviders.map((factory) => `- "${factory.name}": ${factory.description}`),
92
+ ].join("\n"),
89
93
  inputSchema: zodToJsonSchema(z.object({
90
- name: z.enum(this._mcpProviders.map(factory => factory.name)).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'),
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"),
91
98
  }), { strictUnions: true }),
92
99
  annotations: {
93
- title: 'Connect to a browser context',
100
+ title: "Connect to a browser context",
94
101
  readOnlyHint: true,
95
102
  openWorldHint: false,
96
103
  },
@@ -99,17 +106,25 @@ export class ProxyBackend {
99
106
  async _setCurrentClient(factory) {
100
107
  await this._currentClient?.close();
101
108
  this._currentClient = undefined;
102
- const client = new Client({ name: 'Playwright MCP Proxy', version: packageJSON.version });
109
+ const client = new Client({
110
+ name: "Playwright MCP Proxy",
111
+ version: packageJSON.version,
112
+ });
103
113
  client.registerCapabilities({
104
114
  roots: {
105
- listRoots: true,
115
+ listChanged: true,
106
116
  },
107
117
  });
108
- client.setRequestHandler(ListRootsRequestSchema, () => ({ roots: this._roots }));
118
+ client.setRequestHandler(ListRootsRequestSchema, () => ({
119
+ roots: this._roots,
120
+ }));
109
121
  client.setRequestHandler(PingRequestSchema, () => ({}));
110
122
  const transport = await factory.connect();
111
123
  await client.connect(transport);
112
124
  this._currentClient = client;
113
125
  }
114
126
  }
115
- const clientsWithRoots = ['Visual Studio Code', 'Visual Studio Code - Insiders'];
127
+ const clientsWithRoots = [
128
+ "Visual Studio Code",
129
+ "Visual Studio Code - Insiders",
130
+ ];
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
  };