@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,9 +1,9 @@
1
- import { pipe, memoize } from '@fxts/core';
2
- import { buildLexer, apply, seq, alt_sc, rep_sc, tok, kmid, extractByTokenRange, makeParserModule } from 'typescript-parsec';
1
+ import { memoize, pipe } from "@fxts/core";
2
+ import { alt_sc, apply, buildLexer, extractByTokenRange, kmid, makeParserModule, rep_sc, seq, tok, } from "typescript-parsec";
3
3
  // Pure utility functions
4
- export const escapeString = (str) => str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
5
- const isBlank = (str) => str.trim() === '';
6
- export const createHelperCall = (helper, ...args) => `__pwHelpers.${helper}(${args.map(a => `'${escapeString(a)}'`).join(', ')})`;
4
+ export const escapeString = (str) => str.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
5
+ const isBlank = (str) => str.trim() === "";
6
+ export const createHelperCall = (helper, ...args) => `__pwHelpers.${helper}(${args.map((a) => `'${escapeString(a)}'`).join(", ")})`;
7
7
  var SelTokKind;
8
8
  (function (SelTokKind) {
9
9
  SelTokKind[SelTokKind["HasText"] = 0] = "HasText";
@@ -36,49 +36,49 @@ const selectorLexer = buildLexer([
36
36
  [true, /^"(?:[^"\\]|\\.)*"/g, SelTokKind.DQString],
37
37
  [true, /^'(?:[^'\\]|\\.)*'/g, SelTokKind.SQString],
38
38
  [true, /^`(?:[^`\\]|\\.)*`/g, SelTokKind.BQString],
39
- [true, /^\/(?:[^\/\\]|\\.)+\/[a-z]*/gi, SelTokKind.Regex],
40
- [true, /^[^]/g, SelTokKind.Other],
39
+ [true, /^\/(?:[^/\\]|\\.)+\/[a-z]*/gi, SelTokKind.Regex],
40
+ [true, /^[\s\S]/g, SelTokKind.Other],
41
41
  ]);
42
42
  // text args can be "..." or '...' or `...` or /.../flags
43
- const TEXT_ARG = alt_sc(apply(tok(SelTokKind.DQString), t => t.text.slice(1, t.text.length - 1)), apply(tok(SelTokKind.SQString), t => t.text.slice(1, t.text.length - 1)), apply(tok(SelTokKind.BQString), t => t.text.slice(1, t.text.length - 1)), apply(tok(SelTokKind.Regex), t => t.text));
43
+ const TEXT_ARG = alt_sc(apply(tok(SelTokKind.DQString), (t) => t.text.slice(1, t.text.length - 1)), apply(tok(SelTokKind.SQString), (t) => t.text.slice(1, t.text.length - 1)), apply(tok(SelTokKind.BQString), (t) => t.text.slice(1, t.text.length - 1)), apply(tok(SelTokKind.Regex), (t) => t.text));
44
44
  // Keep for generic quoted (used where we want the raw quoted text, not regex)
45
45
  const QUOTED = alt_sc(tok(SelTokKind.DQString), tok(SelTokKind.SQString), tok(SelTokKind.BQString));
46
46
  // Recursive string parser for :has(...) argument
47
47
  const HasArgModule = makeParserModule({
48
- ITEM: m => alt_sc(apply(QUOTED, t => t.text), apply(tok(SelTokKind.Regex), t => t.text), // allow regex inside :has inner
49
- apply(tok(SelTokKind.Has), t => t.text), apply(tok(SelTokKind.HasText), t => t.text), apply(tok(SelTokKind.Contains), t => t.text), apply(tok(SelTokKind.Text), t => t.text), apply(tok(SelTokKind.Visible), t => t.text), apply(tok(SelTokKind.NthMatch), t => t.text), apply(tok(SelTokKind.Comma), t => t.text), // allow comma in inner (selector lists)
50
- apply(tok(SelTokKind.Other), t => t.text), apply(seq(tok(SelTokKind.LParen), m.ARG, tok(SelTokKind.RParen)), v => '(' + v[1] + ')')),
51
- ARG: m => apply(rep_sc(m.ITEM), arr => arr.join('')),
48
+ ITEM: (m) => alt_sc(apply(QUOTED, (t) => t.text), apply(tok(SelTokKind.Regex), (t) => t.text), // allow regex inside :has inner
49
+ apply(tok(SelTokKind.Has), (t) => t.text), apply(tok(SelTokKind.HasText), (t) => t.text), apply(tok(SelTokKind.Contains), (t) => t.text), apply(tok(SelTokKind.Text), (t) => t.text), apply(tok(SelTokKind.Visible), (t) => t.text), apply(tok(SelTokKind.NthMatch), (t) => t.text), apply(tok(SelTokKind.Comma), (t) => t.text), // allow comma in inner (selector lists)
50
+ apply(tok(SelTokKind.Other), (t) => t.text), apply(seq(tok(SelTokKind.LParen), m.ARG, tok(SelTokKind.RParen)), (v) => "(" + v[1] + ")")),
51
+ ARG: (m) => apply(rep_sc(m.ITEM), (arr) => arr.join("")),
52
52
  });
53
53
  // first arg of :nth-match — same as ARG but *without* comma tokens
54
54
  const HasArgNoCommaModule = makeParserModule({
55
- ITEM: m => alt_sc(apply(QUOTED, t => t.text), apply(tok(SelTokKind.Regex), t => t.text), apply(tok(SelTokKind.Has), t => t.text), apply(tok(SelTokKind.HasText), t => t.text), apply(tok(SelTokKind.Contains), t => t.text), apply(tok(SelTokKind.Text), t => t.text), apply(tok(SelTokKind.Visible), t => t.text), apply(tok(SelTokKind.NthMatch), t => t.text), apply(tok(SelTokKind.Other), t => t.text), apply(seq(tok(SelTokKind.LParen), m.ARG, tok(SelTokKind.RParen)), v => '(' + v[1] + ')')),
56
- ARG: m => apply(rep_sc(m.ITEM), arr => arr.join('')),
55
+ ITEM: (m) => alt_sc(apply(QUOTED, (t) => t.text), apply(tok(SelTokKind.Regex), (t) => t.text), apply(tok(SelTokKind.Has), (t) => t.text), apply(tok(SelTokKind.HasText), (t) => t.text), apply(tok(SelTokKind.Contains), (t) => t.text), apply(tok(SelTokKind.Text), (t) => t.text), apply(tok(SelTokKind.Visible), (t) => t.text), apply(tok(SelTokKind.NthMatch), (t) => t.text), apply(tok(SelTokKind.Other), (t) => t.text), apply(seq(tok(SelTokKind.LParen), m.ARG, tok(SelTokKind.RParen)), (v) => "(" + v[1] + ")")),
56
+ ARG: (m) => apply(rep_sc(m.ITEM), (arr) => arr.join("")),
57
57
  });
58
58
  const HAS_ARG = HasArgModule.ARG;
59
59
  const NTH_ARG_NO_COMMA = HasArgNoCommaModule.ARG;
60
60
  const PseudoModule = makeParserModule({
61
- HAS: () => apply(kmid(seq(tok(SelTokKind.Has), tok(SelTokKind.LParen)), HAS_ARG, tok(SelTokKind.RParen)), inner => ({ kind: 'has', inner })),
62
- HAS_TEXT: () => apply(kmid(seq(tok(SelTokKind.HasText), tok(SelTokKind.LParen)), TEXT_ARG, tok(SelTokKind.RParen)), text => ({ kind: 'hasText', text })),
63
- CONTAINS: () => apply(kmid(seq(tok(SelTokKind.Contains), tok(SelTokKind.LParen)), TEXT_ARG, tok(SelTokKind.RParen)), text => ({ kind: 'contains', text })),
64
- TEXT: () => apply(kmid(seq(tok(SelTokKind.Text), tok(SelTokKind.LParen)), TEXT_ARG, tok(SelTokKind.RParen)), text => ({ kind: 'text', text })),
65
- VISIBLE: () => apply(tok(SelTokKind.Visible), () => ({ kind: 'visible' })),
61
+ HAS: () => apply(kmid(seq(tok(SelTokKind.Has), tok(SelTokKind.LParen)), HAS_ARG, tok(SelTokKind.RParen)), (inner) => ({ kind: "has", inner })),
62
+ HAS_TEXT: () => apply(kmid(seq(tok(SelTokKind.HasText), tok(SelTokKind.LParen)), TEXT_ARG, tok(SelTokKind.RParen)), (text) => ({ kind: "hasText", text })),
63
+ CONTAINS: () => apply(kmid(seq(tok(SelTokKind.Contains), tok(SelTokKind.LParen)), TEXT_ARG, tok(SelTokKind.RParen)), (text) => ({ kind: "contains", text })),
64
+ TEXT: () => apply(kmid(seq(tok(SelTokKind.Text), tok(SelTokKind.LParen)), TEXT_ARG, tok(SelTokKind.RParen)), (text) => ({ kind: "text", text })),
65
+ VISIBLE: () => apply(tok(SelTokKind.Visible), () => ({ kind: "visible" })),
66
66
  // :nth-match(<selector>, <index>)
67
67
  NTH_MATCH: () => apply(kmid(seq(tok(SelTokKind.NthMatch), tok(SelTokKind.LParen)), seq(NTH_ARG_NO_COMMA, // first arg string (no commas)
68
- apply(rep_sc(tok(SelTokKind.WS)), () => ''), // optional spaces
69
- tok(SelTokKind.Comma), apply(rep_sc(tok(SelTokKind.WS)), () => ''), // optional spaces
70
- TEXT_ARG), tok(SelTokKind.RParen)), v => {
68
+ apply(rep_sc(tok(SelTokKind.WS)), () => ""), // optional spaces
69
+ tok(SelTokKind.Comma), apply(rep_sc(tok(SelTokKind.WS)), () => ""), // optional spaces
70
+ TEXT_ARG), tok(SelTokKind.RParen)), (v) => {
71
71
  const inner = v[0];
72
72
  const idxRaw = v[4];
73
- const index = parseInt(idxRaw, 10);
74
- return { kind: 'nthMatch', inner, index: isFinite(index) ? index : 1 };
73
+ const index = Number.parseInt(idxRaw, 10);
74
+ return { kind: "nthMatch", inner, index: isFinite(index) ? index : 1 };
75
75
  }),
76
- ANY: m => alt_sc(m.HAS, m.HAS_TEXT, m.CONTAINS, m.TEXT, m.VISIBLE, m.NTH_MATCH),
76
+ ANY: (m) => alt_sc(m.HAS, m.HAS_TEXT, m.CONTAINS, m.TEXT, m.VISIBLE, m.NTH_MATCH),
77
77
  });
78
78
  const parseFirstPseudo = (selector) => {
79
79
  const start = selectorLexer.parse(selector);
80
80
  if (!start)
81
- return { kind: 'none' };
81
+ return { kind: "none" };
82
82
  let t = start;
83
83
  while (t) {
84
84
  const parsed = PseudoModule.ANY.parse(t);
@@ -88,21 +88,21 @@ const parseFirstPseudo = (selector) => {
88
88
  const prefix = selector.slice(0, t.pos.index);
89
89
  const base = prefix + selector.slice(t.pos.index + consumed);
90
90
  const res = cand.result;
91
- if (res.kind === 'has')
92
- return { kind: 'has', base, inner: res.inner };
93
- if (res.kind === 'visible')
94
- return { kind: 'visible', base };
95
- if (res.kind === 'hasText')
96
- return { kind: 'hasText', base, text: res.text };
97
- if (res.kind === 'contains')
98
- return { kind: 'contains', base, text: res.text };
99
- if (res.kind === 'nthMatch')
100
- return { kind: 'nthMatch', base, inner: res.inner, index: res.index };
101
- return { kind: 'text', base, text: res.text };
91
+ if (res.kind === "has")
92
+ return { kind: "has", base, inner: res.inner };
93
+ if (res.kind === "visible")
94
+ return { kind: "visible", base };
95
+ if (res.kind === "hasText")
96
+ return { kind: "hasText", base, text: res.text };
97
+ if (res.kind === "contains")
98
+ return { kind: "contains", base, text: res.text };
99
+ if (res.kind === "nthMatch")
100
+ return { kind: "nthMatch", base, inner: res.inner, index: res.index };
101
+ return { kind: "text", base, text: res.text };
102
102
  }
103
103
  t = t.next;
104
104
  }
105
- return { kind: 'none' };
105
+ return { kind: "none" };
106
106
  };
107
107
  var SkipTokKind;
108
108
  (function (SkipTokKind) {
@@ -122,7 +122,7 @@ var SkipTokKind;
122
122
  })(SkipTokKind || (SkipTokKind = {}));
123
123
  const skipLexer = buildLexer([
124
124
  [true, /^\/\/[^\n]*/g, SkipTokKind.LineComment],
125
- [true, /^\/\*[^]*?\*\//g, SkipTokKind.BlockComment],
125
+ [true, /^\/\*[\s\S]*?\*\//g, SkipTokKind.BlockComment],
126
126
  [true, /^"(?:[^"\\]|\\.)*"/g, SkipTokKind.DQString],
127
127
  [true, /^'(?:[^'\\]|\\.)*'/g, SkipTokKind.SQString],
128
128
  [true, /^`(?:[^`\\]|\\.)*`/g, SkipTokKind.BQString],
@@ -133,39 +133,57 @@ const skipLexer = buildLexer([
133
133
  [true, /^querySelectorAll\b/g, SkipTokKind.QuerySelectorAll],
134
134
  [true, /^querySelector\b/g, SkipTokKind.QuerySelector],
135
135
  [true, /^[A-Za-z_$][A-Za-z0-9_$]*/g, SkipTokKind.Identifier],
136
- [true, /^[^]/g, SkipTokKind.Other],
136
+ [true, /^[\s\S]/g, SkipTokKind.Other],
137
137
  ]);
138
138
  const SKIP_CALL_QUOTED = alt_sc(tok(SkipTokKind.DQString), tok(SkipTokKind.SQString), tok(SkipTokKind.BQString));
139
139
  // Zero-or-more space/comments -> string
140
140
  const WS_OR_COMMENT = alt_sc(tok(SkipTokKind.WS), tok(SkipTokKind.LineComment), tok(SkipTokKind.BlockComment));
141
- const SP0 = apply(rep_sc(WS_OR_COMMENT), ts => ts.map(t => t.text).join(''));
141
+ const SP0 = apply(rep_sc(WS_OR_COMMENT), (ts) => ts.map((t) => t.text).join(""));
142
142
  // receiver := Identifier ('.' Identifier)*
143
143
  const RECEIVER = apply(seq(tok(SkipTokKind.Identifier), rep_sc(seq(tok(SkipTokKind.Dot), tok(SkipTokKind.Identifier)))), (v) => {
144
144
  const head = v[0].text;
145
- const tail = v[1].map((p) => p[0].text + p[1].text).join('');
145
+ const tail = v[1]
146
+ .map((p) => p[0].text + p[1].text)
147
+ .join("");
146
148
  return head + tail;
147
149
  });
148
150
  const SegmentModule = makeParserModule({
149
- RAW: () => apply(alt_sc(tok(SkipTokKind.LParen), tok(SkipTokKind.RParen), tok(SkipTokKind.WS), tok(SkipTokKind.LineComment), tok(SkipTokKind.BlockComment), tok(SkipTokKind.DQString), tok(SkipTokKind.SQString), tok(SkipTokKind.BQString), tok(SkipTokKind.QuerySelectorAll), tok(SkipTokKind.QuerySelector), tok(SkipTokKind.Identifier), tok(SkipTokKind.Dot), tok(SkipTokKind.Other)), t => ({ kind: 'raw', text: t.text })),
150
- CALL_ONE: () => apply(seq(RECEIVER, tok(SkipTokKind.Dot), tok(SkipTokKind.QuerySelector), SP0, tok(SkipTokKind.LParen), SP0, SKIP_CALL_QUOTED, SP0, tok(SkipTokKind.RParen)), v => {
151
+ RAW: () => apply(alt_sc(tok(SkipTokKind.LParen), tok(SkipTokKind.RParen), tok(SkipTokKind.WS), tok(SkipTokKind.LineComment), tok(SkipTokKind.BlockComment), tok(SkipTokKind.DQString), tok(SkipTokKind.SQString), tok(SkipTokKind.BQString), tok(SkipTokKind.QuerySelectorAll), tok(SkipTokKind.QuerySelector), tok(SkipTokKind.Identifier), tok(SkipTokKind.Dot), tok(SkipTokKind.Other)), (t) => ({ kind: "raw", text: t.text })),
152
+ CALL_ONE: () => apply(seq(RECEIVER, tok(SkipTokKind.Dot), tok(SkipTokKind.QuerySelector), SP0, tok(SkipTokKind.LParen), SP0, SKIP_CALL_QUOTED, SP0, tok(SkipTokKind.RParen)), (v) => {
151
153
  const lit = v[6];
152
154
  return {
153
- kind: 'call',
154
- method: 'one',
155
+ kind: "call",
156
+ method: "one",
155
157
  selector: lit.text.slice(1, lit.text.length - 1),
156
- original: v[0] + v[1].text + v[2].text + v[3] + v[4].text + v[5] + v[6].text + v[7] + v[8].text,
158
+ original: v[0] +
159
+ v[1].text +
160
+ v[2].text +
161
+ v[3] +
162
+ v[4].text +
163
+ v[5] +
164
+ v[6].text +
165
+ v[7] +
166
+ v[8].text,
157
167
  };
158
168
  }),
159
- CALL_ALL: () => apply(seq(RECEIVER, tok(SkipTokKind.Dot), tok(SkipTokKind.QuerySelectorAll), SP0, tok(SkipTokKind.LParen), SP0, SKIP_CALL_QUOTED, SP0, tok(SkipTokKind.RParen)), v => {
169
+ CALL_ALL: () => apply(seq(RECEIVER, tok(SkipTokKind.Dot), tok(SkipTokKind.QuerySelectorAll), SP0, tok(SkipTokKind.LParen), SP0, SKIP_CALL_QUOTED, SP0, tok(SkipTokKind.RParen)), (v) => {
160
170
  const lit = v[6];
161
171
  return {
162
- kind: 'call',
163
- method: 'all',
172
+ kind: "call",
173
+ method: "all",
164
174
  selector: lit.text.slice(1, lit.text.length - 1),
165
- original: v[0] + v[1].text + v[2].text + v[3] + v[4].text + v[5] + v[6].text + v[7] + v[8].text,
175
+ original: v[0] +
176
+ v[1].text +
177
+ v[2].text +
178
+ v[3] +
179
+ v[4].text +
180
+ v[5] +
181
+ v[6].text +
182
+ v[7] +
183
+ v[8].text,
166
184
  };
167
185
  }),
168
- SEGMENT: m => alt_sc(m.CALL_ALL, m.CALL_ONE, m.RAW),
186
+ SEGMENT: (m) => alt_sc(m.CALL_ALL, m.CALL_ONE, m.RAW),
169
187
  });
170
188
  const PROGRAM = rep_sc(SegmentModule.SEGMENT);
171
189
  // Helper functions template
@@ -286,21 +304,21 @@ export const helperFunctions = `
286
304
  `;
287
305
  // ----- Transformers -----
288
306
  export const transformHasText = ({ script, needsHelpers }) => {
289
- if (!script.includes(':has-text('))
307
+ if (!script.includes(":has-text("))
290
308
  return { script, needsHelpers };
291
309
  const result = transformSelectorCalls(script, (selector, method) => {
292
310
  const parsed = parseFirstPseudo(selector);
293
- if (parsed.kind !== 'hasText')
311
+ if (parsed.kind !== "hasText")
294
312
  return null;
295
313
  // if base is empty, behave like :text() — search globally
296
314
  if (isBlank(parsed.base)) {
297
- return method === 'all'
298
- ? createHelperCall('hasTextAll', '*', parsed.text)
299
- : createHelperCall('text', parsed.text);
315
+ return method === "all"
316
+ ? createHelperCall("hasTextAll", "*", parsed.text)
317
+ : createHelperCall("text", parsed.text);
300
318
  }
301
- return method === 'all'
302
- ? createHelperCall('hasTextAll', parsed.base, parsed.text)
303
- : createHelperCall('hasText', parsed.base, parsed.text);
319
+ return method === "all"
320
+ ? createHelperCall("hasTextAll", parsed.base, parsed.text)
321
+ : createHelperCall("hasText", parsed.base, parsed.text);
304
322
  });
305
323
  return {
306
324
  script: result.output,
@@ -308,15 +326,15 @@ export const transformHasText = ({ script, needsHelpers }) => {
308
326
  };
309
327
  };
310
328
  export const transformVisible = ({ script, needsHelpers }) => {
311
- if (!script.includes(':visible'))
329
+ if (!script.includes(":visible"))
312
330
  return { script, needsHelpers };
313
331
  const result = transformSelectorCalls(script, (selector, method) => {
314
332
  const parsed = parseFirstPseudo(selector);
315
- if (parsed.kind !== 'visible')
333
+ if (parsed.kind !== "visible")
316
334
  return null;
317
- return method === 'all'
318
- ? createHelperCall('visibleAll', parsed.base)
319
- : createHelperCall('visible', parsed.base);
335
+ return method === "all"
336
+ ? createHelperCall("visibleAll", parsed.base)
337
+ : createHelperCall("visible", parsed.base);
320
338
  });
321
339
  return {
322
340
  script: result.output,
@@ -324,20 +342,20 @@ export const transformVisible = ({ script, needsHelpers }) => {
324
342
  };
325
343
  };
326
344
  export const transformText = ({ script, needsHelpers }) => {
327
- if (!script.includes(':text('))
345
+ if (!script.includes(":text("))
328
346
  return { script, needsHelpers };
329
347
  const result = transformSelectorCalls(script, (selector, method) => {
330
348
  const parsed = parseFirstPseudo(selector);
331
- if (parsed.kind !== 'text')
349
+ if (parsed.kind !== "text")
332
350
  return null;
333
351
  if (isBlank(parsed.base)) {
334
- return method === 'all'
335
- ? createHelperCall('hasTextAll', '*', parsed.text)
336
- : createHelperCall('text', parsed.text);
352
+ return method === "all"
353
+ ? createHelperCall("hasTextAll", "*", parsed.text)
354
+ : createHelperCall("text", parsed.text);
337
355
  }
338
- return method === 'all'
339
- ? createHelperCall('hasTextAll', parsed.base, parsed.text)
340
- : createHelperCall('hasText', parsed.base, parsed.text);
356
+ return method === "all"
357
+ ? createHelperCall("hasTextAll", parsed.base, parsed.text)
358
+ : createHelperCall("hasText", parsed.base, parsed.text);
341
359
  });
342
360
  return {
343
361
  script: result.output,
@@ -353,42 +371,47 @@ export const transformTextEngine = ({ script, needsHelpers }) => {
353
371
  if (m1) {
354
372
  const arg = m1[1].trim();
355
373
  const val = unwrapPossibleQuotesOrKeepRegex(arg);
356
- return method === 'all'
357
- ? createHelperCall('textAll', val)
358
- : createHelperCall('text', val);
374
+ return method === "all"
375
+ ? createHelperCall("textAll", val)
376
+ : createHelperCall("text", val);
359
377
  }
360
378
  // Case 2: X >> text=...
361
379
  const m2 = raw.match(/^(.*?)>>\s*text\s*=\s*(.+)\s*$/);
362
380
  if (m2) {
363
- const base = m2[1].trim() || '*';
381
+ const base = m2[1].trim() || "*";
364
382
  const arg = m2[2].trim();
365
383
  const val = unwrapPossibleQuotesOrKeepRegex(arg);
366
- return method === 'all'
367
- ? createHelperCall('hasTextAll', base, val)
368
- : createHelperCall('hasText', base, val);
384
+ return method === "all"
385
+ ? createHelperCall("hasTextAll", base, val)
386
+ : createHelperCall("hasText", base, val);
369
387
  }
370
388
  return null;
371
389
  });
372
- return { script: result.output, needsHelpers: needsHelpers || result.changed };
390
+ return {
391
+ script: result.output,
392
+ needsHelpers: needsHelpers || result.changed,
393
+ };
373
394
  };
374
395
  // Utility used only by transformTextEngine
375
396
  const unwrapPossibleQuotesOrKeepRegex = (s) => {
376
397
  const q = s[0];
377
- if ((q === '"' || q === "'" || q === '`') && s.length >= 2 && s[s.length - 1] === q)
398
+ if ((q === '"' || q === "'" || q === "`") &&
399
+ s.length >= 2 &&
400
+ s[s.length - 1] === q)
378
401
  return s.slice(1, -1);
379
402
  // keep /.../flags as-is
380
403
  return s;
381
404
  };
382
405
  export const transformContains = ({ script, needsHelpers }) => {
383
- if (!script.includes(':contains('))
406
+ if (!script.includes(":contains("))
384
407
  return { script, needsHelpers };
385
408
  const result = transformSelectorCalls(script, (selector, method) => {
386
409
  const parsed = parseFirstPseudo(selector);
387
- if (parsed.kind !== 'contains')
410
+ if (parsed.kind !== "contains")
388
411
  return null;
389
- return method === 'all'
390
- ? createHelperCall('hasTextAll', parsed.base, parsed.text)
391
- : createHelperCall('hasText', parsed.base, parsed.text);
412
+ return method === "all"
413
+ ? createHelperCall("hasTextAll", parsed.base, parsed.text)
414
+ : createHelperCall("hasText", parsed.base, parsed.text);
392
415
  });
393
416
  return {
394
417
  script: result.output,
@@ -396,15 +419,15 @@ export const transformContains = ({ script, needsHelpers }) => {
396
419
  };
397
420
  };
398
421
  export const transformHas = ({ script, needsHelpers }) => {
399
- if (!script.includes(':has('))
422
+ if (!script.includes(":has("))
400
423
  return { script, needsHelpers };
401
424
  const result = transformSelectorCalls(script, (selector, method) => {
402
425
  const parsed = parseFirstPseudo(selector);
403
- if (parsed.kind !== 'has')
426
+ if (parsed.kind !== "has")
404
427
  return null;
405
- return method === 'all'
406
- ? createHelperCall('hasAll', parsed.base, parsed.inner)
407
- : createHelperCall('has', parsed.base, parsed.inner);
428
+ return method === "all"
429
+ ? createHelperCall("hasAll", parsed.base, parsed.inner)
430
+ : createHelperCall("has", parsed.base, parsed.inner);
408
431
  });
409
432
  return {
410
433
  script: result.output,
@@ -413,17 +436,17 @@ export const transformHas = ({ script, needsHelpers }) => {
413
436
  };
414
437
  // :nth-match(...)
415
438
  export const transformNthMatch = ({ script, needsHelpers }) => {
416
- if (!script.includes(':nth-match('))
439
+ if (!script.includes(":nth-match("))
417
440
  return { script, needsHelpers };
418
441
  const result = transformSelectorCalls(script, (selector, method) => {
419
442
  const parsed = parseFirstPseudo(selector);
420
- if (parsed.kind !== 'nthMatch')
443
+ if (parsed.kind !== "nthMatch")
421
444
  return null;
422
445
  // If base is empty, treat as global
423
- const base = (parsed.base || '').trim();
424
- if (method === 'all')
425
- return createHelperCall('nthMatchAll', base, parsed.inner, String(parsed.index));
426
- return createHelperCall('nthMatch', base, parsed.inner, String(parsed.index));
446
+ const base = (parsed.base || "").trim();
447
+ if (method === "all")
448
+ return createHelperCall("nthMatchAll", base, parsed.inner, String(parsed.index));
449
+ return createHelperCall("nthMatch", base, parsed.inner, String(parsed.index));
427
450
  });
428
451
  return {
429
452
  script: result.output,
@@ -431,14 +454,14 @@ export const transformNthMatch = ({ script, needsHelpers }) => {
431
454
  };
432
455
  };
433
456
  export const transformDescendant = ({ script, needsHelpers }) => {
434
- if (!script.includes('>>'))
457
+ if (!script.includes(">>"))
435
458
  return { script, needsHelpers };
436
459
  const result = transformSelectorCalls(script, (selector, method) => {
437
- if (!selector.includes('>>'))
460
+ if (!selector.includes(">>"))
438
461
  return null;
439
- return method === 'all'
440
- ? createHelperCall('querySelectorAll', selector)
441
- : createHelperCall('querySelector', selector);
462
+ return method === "all"
463
+ ? createHelperCall("querySelectorAll", selector)
464
+ : createHelperCall("querySelector", selector);
442
465
  });
443
466
  return {
444
467
  script: result.output,
@@ -449,7 +472,7 @@ export const transformDescendant = ({ script, needsHelpers }) => {
449
472
  export const injectHelpers = ({ script, needsHelpers }) => needsHelpers
450
473
  ? {
451
474
  script: helperFunctions +
452
- '\n/* Playwright selectors automatically transformed */\n' +
475
+ "\n/* Playwright selectors automatically transformed */\n" +
453
476
  script,
454
477
  needsHelpers,
455
478
  }
@@ -479,7 +502,7 @@ const transformSelectorCalls = (code, rewriter) => {
479
502
  let changed = false;
480
503
  const out = [];
481
504
  for (const seg of segments) {
482
- if (seg.kind === 'call') {
505
+ if (seg.kind === "call") {
483
506
  const replacement = rewriter(seg.selector, seg.method);
484
507
  if (replacement) {
485
508
  out.push(replacement);
@@ -493,5 +516,5 @@ const transformSelectorCalls = (code, rewriter) => {
493
516
  out.push(seg.text);
494
517
  }
495
518
  }
496
- return { output: out.join(''), changed };
519
+ return { output: out.join(""), changed };
497
520
  };
package/lib/program.js CHANGED
@@ -13,59 +13,59 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { program, Option } from 'commander';
17
- import * as mcpServer from './mcp/server.js';
18
- import * as mcpTransport from './mcp/transport.js';
19
- import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
20
- import { packageJSON } from './utils/package.js';
21
- import { Context } from './context.js';
22
- import { contextFactory } from './browserContextFactory.js';
23
- import { ProxyBackend } from './mcp/proxyBackend.js';
24
- import { BrowserServerBackend } from './browserServerBackend.js';
25
- import { ExtensionContextFactory } from './extension/extensionContextFactory.js';
26
- import { InProcessTransport } from './mcp/inProcessTransport.js';
16
+ import { Option, program } from "commander";
17
+ import { contextFactory } from "./browserContextFactory.js";
18
+ import { BrowserServerBackend } from "./browserServerBackend.js";
19
+ import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList, } from "./config.js";
20
+ import { Context } from "./context.js";
21
+ import { ExtensionContextFactory } from "./extension/extensionContextFactory.js";
22
+ import { InProcessTransport } from "./mcp/inProcessTransport.js";
23
+ import { ProxyBackend } from "./mcp/proxyBackend.js";
24
+ import * as mcpServer from "./mcp/server.js";
25
+ import * as mcpTransport from "./mcp/transport.js";
26
+ import { packageJSON } from "./utils/package.js";
27
27
  program
28
- .version('Version ' + packageJSON.version)
28
+ .version("Version " + packageJSON.version)
29
29
  .name(packageJSON.name)
30
- .option('--allowed-origins <origins>', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
31
- .option('--blocked-origins <origins>', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList)
32
- .option('--block-service-workers', 'block service workers')
33
- .option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
34
- .option('--caps <caps>', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList)
35
- .option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
36
- .option('--config <path>', 'path to the configuration file.')
37
- .option('--device <device>', 'device to emulate, for example: "iPhone 15"')
38
- .option('--executable-path <path>', 'path to the browser executable.')
39
- .option('--headless', 'run browser in headless mode, headed by default')
40
- .option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
41
- .option('--ignore-https-errors', 'ignore https errors')
42
- .option('--init-script <path>', 'path to a JavaScript file to inject into all pages using addInitScript.')
43
- .option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
44
- .option('--image-responses <mode>', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".')
45
- .option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')
46
- .option('--output-dir <path>', 'path to the directory for output files.')
47
- .option('--port <port>', 'port to listen on for SSE transport.')
48
- .option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
49
- .option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
50
- .option('--save-session', 'Whether to save the Playwright MCP session into the output directory.')
51
- .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.')
52
- .option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
53
- .option('--user-agent <ua string>', 'specify user agent string')
54
- .option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
55
- .option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
56
- .option('--window-position <x,y>', 'specify Chrome window position in pixels, for example "100,200"')
57
- .option('--window-size <width,height>', 'specify Chrome window size in pixels, for example "1280,720"')
58
- .option('--app <url>', 'launch browser in app mode with the specified URL')
59
- .addOption(new Option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').hideHelp())
60
- .addOption(new Option('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp())
61
- .addOption(new Option('--loop-tools', 'Run loop tools').hideHelp())
62
- .addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
30
+ .option("--allowed-origins <origins>", "semicolon-separated list of origins to allow the browser to request. Default is to allow all.", semicolonSeparatedList)
31
+ .option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.", semicolonSeparatedList)
32
+ .option("--block-service-workers", "block service workers")
33
+ .option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.")
34
+ .option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf.", commaSeparatedList)
35
+ .option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.")
36
+ .option("--config <path>", "path to the configuration file.")
37
+ .option("--device <device>", 'device to emulate, for example: "iPhone 15"')
38
+ .option("--executable-path <path>", "path to the browser executable.")
39
+ .option("--headless", "run browser in headless mode, headed by default")
40
+ .option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.")
41
+ .option("--ignore-https-errors", "ignore https errors")
42
+ .option("--init-script <path>", "path to a JavaScript file to inject into all pages using addInitScript.")
43
+ .option("--isolated", "keep the browser profile in memory, do not save it to disk.")
44
+ .option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".')
45
+ .option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.")
46
+ .option("--output-dir <path>", "path to the directory for output files.")
47
+ .option("--port <port>", "port to listen on for SSE transport.")
48
+ .option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
49
+ .option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
50
+ .option("--save-session", "Whether to save the Playwright MCP session into the output directory.")
51
+ .option("--save-trace", "Whether to save the Playwright Trace of the session into the output directory.")
52
+ .option("--storage-state <path>", "path to the storage state file for isolated sessions.")
53
+ .option("--user-agent <ua string>", "specify user agent string")
54
+ .option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.")
55
+ .option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280, 720"')
56
+ .option("--window-position <x,y>", 'specify Chrome window position in pixels, for example "100,200"')
57
+ .option("--window-size <width,height>", 'specify Chrome window size in pixels, for example "1280,720"')
58
+ .option("--app <url>", "launch browser in app mode with the specified URL")
59
+ .addOption(new Option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').hideHelp())
60
+ .addOption(new Option("--connect-tool", "Allow to switch between different browser connection methods.").hideHelp())
61
+ .addOption(new Option("--loop-tools", "Run loop tools").hideHelp())
62
+ .addOption(new Option("--vision", "Legacy option, use --caps=vision instead").hideHelp())
63
63
  .action(async (options) => {
64
64
  setupExitWatchdog();
65
65
  if (options.vision) {
66
66
  // eslint-disable-next-line no-console
67
- console.error('The --vision option is deprecated, use --caps=vision instead');
68
- options.caps = 'vision';
67
+ console.error("The --vision option is deprecated, use --caps=vision instead");
68
+ options.caps = "vision";
69
69
  }
70
70
  const config = await resolveCLIConfig(options);
71
71
  if (options.extension) {
@@ -75,7 +75,9 @@ program
75
75
  return;
76
76
  }
77
77
  const browserContextFactory = contextFactory(config);
78
- const providers = [mcpProviderForBrowserContextFactory(config, browserContextFactory)];
78
+ const providers = [
79
+ mcpProviderForBrowserContextFactory(config, browserContextFactory),
80
+ ];
79
81
  if (options.connectTool)
80
82
  providers.push(mcpProviderForBrowserContextFactory(config, createExtensionContextFactory(config)));
81
83
  await mcpTransport.start(() => new ProxyBackend(providers), config.server);
@@ -90,12 +92,12 @@ function setupExitWatchdog() {
90
92
  await Context.disposeAll();
91
93
  process.exit(0);
92
94
  };
93
- process.stdin.on('close', handleExit);
94
- process.on('SIGINT', handleExit);
95
- process.on('SIGTERM', handleExit);
95
+ process.stdin.on("close", handleExit);
96
+ process.on("SIGINT", handleExit);
97
+ process.on("SIGTERM", handleExit);
96
98
  }
97
99
  function createExtensionContextFactory(config) {
98
- return new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir);
100
+ return new ExtensionContextFactory(config.browser.launchOptions.channel || "chrome", config.browser.userDataDir);
99
101
  }
100
102
  function mcpProviderForBrowserContextFactory(config, browserContextFactory) {
101
103
  return {