@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,520 @@
1
+ import { memoize, pipe } from "@fxts/core";
2
+ import { alt_sc, apply, buildLexer, extractByTokenRange, kmid, makeParserModule, rep_sc, seq, tok, } from "typescript-parsec";
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(", ")})`;
7
+ var SelTokKind;
8
+ (function (SelTokKind) {
9
+ SelTokKind[SelTokKind["HasText"] = 0] = "HasText";
10
+ SelTokKind[SelTokKind["Contains"] = 1] = "Contains";
11
+ SelTokKind[SelTokKind["Text"] = 2] = "Text";
12
+ SelTokKind[SelTokKind["Visible"] = 3] = "Visible";
13
+ SelTokKind[SelTokKind["Has"] = 4] = "Has";
14
+ SelTokKind[SelTokKind["NthMatch"] = 5] = "NthMatch";
15
+ SelTokKind[SelTokKind["LParen"] = 6] = "LParen";
16
+ SelTokKind[SelTokKind["RParen"] = 7] = "RParen";
17
+ SelTokKind[SelTokKind["Comma"] = 8] = "Comma";
18
+ SelTokKind[SelTokKind["WS"] = 9] = "WS";
19
+ SelTokKind[SelTokKind["DQString"] = 10] = "DQString";
20
+ SelTokKind[SelTokKind["SQString"] = 11] = "SQString";
21
+ SelTokKind[SelTokKind["BQString"] = 12] = "BQString";
22
+ SelTokKind[SelTokKind["Regex"] = 13] = "Regex";
23
+ SelTokKind[SelTokKind["Other"] = 14] = "Other";
24
+ })(SelTokKind || (SelTokKind = {}));
25
+ const selectorLexer = buildLexer([
26
+ [true, /^:has-text/g, SelTokKind.HasText],
27
+ [true, /^:contains/g, SelTokKind.Contains],
28
+ [true, /^:text/g, SelTokKind.Text],
29
+ [true, /^:visible\b/g, SelTokKind.Visible],
30
+ [true, /^:has\b/g, SelTokKind.Has],
31
+ [true, /^:nth-match\b/g, SelTokKind.NthMatch],
32
+ [true, /^\(/g, SelTokKind.LParen],
33
+ [true, /^\)/g, SelTokKind.RParen],
34
+ [true, /^,/g, SelTokKind.Comma],
35
+ [false, /^\s+/g, SelTokKind.WS],
36
+ [true, /^"(?:[^"\\]|\\.)*"/g, SelTokKind.DQString],
37
+ [true, /^'(?:[^'\\]|\\.)*'/g, SelTokKind.SQString],
38
+ [true, /^`(?:[^`\\]|\\.)*`/g, SelTokKind.BQString],
39
+ [true, /^\/(?:[^/\\]|\\.)+\/[a-z]*/gi, SelTokKind.Regex],
40
+ [true, /^[\s\S]/g, SelTokKind.Other],
41
+ ]);
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));
44
+ // Keep for generic quoted (used where we want the raw quoted text, not regex)
45
+ const QUOTED = alt_sc(tok(SelTokKind.DQString), tok(SelTokKind.SQString), tok(SelTokKind.BQString));
46
+ // Recursive string parser for :has(...) argument
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("")),
52
+ });
53
+ // first arg of :nth-match — same as ARG but *without* comma tokens
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("")),
57
+ });
58
+ const HAS_ARG = HasArgModule.ARG;
59
+ const NTH_ARG_NO_COMMA = HasArgNoCommaModule.ARG;
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" })),
66
+ // :nth-match(<selector>, <index>)
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) => {
71
+ const inner = v[0];
72
+ const idxRaw = v[4];
73
+ const index = Number.parseInt(idxRaw, 10);
74
+ return { kind: "nthMatch", inner, index: isFinite(index) ? index : 1 };
75
+ }),
76
+ ANY: (m) => alt_sc(m.HAS, m.HAS_TEXT, m.CONTAINS, m.TEXT, m.VISIBLE, m.NTH_MATCH),
77
+ });
78
+ const parseFirstPseudo = (selector) => {
79
+ const start = selectorLexer.parse(selector);
80
+ if (!start)
81
+ return { kind: "none" };
82
+ let t = start;
83
+ while (t) {
84
+ const parsed = PseudoModule.ANY.parse(t);
85
+ if (parsed.successful && parsed.candidates.length > 0) {
86
+ const cand = parsed.candidates[0];
87
+ const consumed = extractByTokenRange(selector, cand.firstToken, cand.nextToken).length;
88
+ const prefix = selector.slice(0, t.pos.index);
89
+ const base = prefix + selector.slice(t.pos.index + consumed);
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 };
102
+ }
103
+ t = t.next;
104
+ }
105
+ return { kind: "none" };
106
+ };
107
+ var SkipTokKind;
108
+ (function (SkipTokKind) {
109
+ SkipTokKind[SkipTokKind["LineComment"] = 0] = "LineComment";
110
+ SkipTokKind[SkipTokKind["BlockComment"] = 1] = "BlockComment";
111
+ SkipTokKind[SkipTokKind["DQString"] = 2] = "DQString";
112
+ SkipTokKind[SkipTokKind["SQString"] = 3] = "SQString";
113
+ SkipTokKind[SkipTokKind["BQString"] = 4] = "BQString";
114
+ SkipTokKind[SkipTokKind["WS"] = 5] = "WS";
115
+ SkipTokKind[SkipTokKind["LParen"] = 6] = "LParen";
116
+ SkipTokKind[SkipTokKind["RParen"] = 7] = "RParen";
117
+ SkipTokKind[SkipTokKind["Dot"] = 8] = "Dot";
118
+ SkipTokKind[SkipTokKind["QuerySelector"] = 9] = "QuerySelector";
119
+ SkipTokKind[SkipTokKind["QuerySelectorAll"] = 10] = "QuerySelectorAll";
120
+ SkipTokKind[SkipTokKind["Identifier"] = 11] = "Identifier";
121
+ SkipTokKind[SkipTokKind["Other"] = 12] = "Other";
122
+ })(SkipTokKind || (SkipTokKind = {}));
123
+ const skipLexer = buildLexer([
124
+ [true, /^\/\/[^\n]*/g, SkipTokKind.LineComment],
125
+ [true, /^\/\*[\s\S]*?\*\//g, SkipTokKind.BlockComment],
126
+ [true, /^"(?:[^"\\]|\\.)*"/g, SkipTokKind.DQString],
127
+ [true, /^'(?:[^'\\]|\\.)*'/g, SkipTokKind.SQString],
128
+ [true, /^`(?:[^`\\]|\\.)*`/g, SkipTokKind.BQString],
129
+ [true, /^\s+/g, SkipTokKind.WS],
130
+ [true, /^\(/g, SkipTokKind.LParen],
131
+ [true, /^\)/g, SkipTokKind.RParen],
132
+ [true, /^\./g, SkipTokKind.Dot],
133
+ [true, /^querySelectorAll\b/g, SkipTokKind.QuerySelectorAll],
134
+ [true, /^querySelector\b/g, SkipTokKind.QuerySelector],
135
+ [true, /^[A-Za-z_$][A-Za-z0-9_$]*/g, SkipTokKind.Identifier],
136
+ [true, /^[\s\S]/g, SkipTokKind.Other],
137
+ ]);
138
+ const SKIP_CALL_QUOTED = alt_sc(tok(SkipTokKind.DQString), tok(SkipTokKind.SQString), tok(SkipTokKind.BQString));
139
+ // Zero-or-more space/comments -> string
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(""));
142
+ // receiver := Identifier ('.' Identifier)*
143
+ const RECEIVER = apply(seq(tok(SkipTokKind.Identifier), rep_sc(seq(tok(SkipTokKind.Dot), tok(SkipTokKind.Identifier)))), (v) => {
144
+ const head = v[0].text;
145
+ const tail = v[1]
146
+ .map((p) => p[0].text + p[1].text)
147
+ .join("");
148
+ return head + tail;
149
+ });
150
+ const SegmentModule = makeParserModule({
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) => {
153
+ const lit = v[6];
154
+ return {
155
+ kind: "call",
156
+ method: "one",
157
+ selector: lit.text.slice(1, lit.text.length - 1),
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,
167
+ };
168
+ }),
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) => {
170
+ const lit = v[6];
171
+ return {
172
+ kind: "call",
173
+ method: "all",
174
+ selector: lit.text.slice(1, lit.text.length - 1),
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,
184
+ };
185
+ }),
186
+ SEGMENT: (m) => alt_sc(m.CALL_ALL, m.CALL_ONE, m.RAW),
187
+ });
188
+ const PROGRAM = rep_sc(SegmentModule.SEGMENT);
189
+ // Helper functions template
190
+ export const helperFunctions = `
191
+ ;(() => {
192
+ if (window.__pwHelpers)
193
+ return;
194
+ window.__pwHelpers = (() => {
195
+ const isVisible = (el) => {
196
+ const s = window.getComputedStyle(el);
197
+ return s.display !== 'none' && s.visibility !== 'hidden' && el.offsetParent !== null;
198
+ };
199
+ // string or /regex/flags -> predicate
200
+ const toMatcher = (needle) => {
201
+ if (typeof needle === 'string' && /^\\/(?:[^\\\\/]|\\\\.)+\\/[a-z]*$/i.test(needle)) {
202
+ const m = needle.match(/^\\/(.*)\\/([a-z]*)$/i);
203
+ try { const re = new RegExp(m[1], m[2]); return (s) => typeof s === 'string' && re.test(s); }
204
+ catch { /* fall through */ }
205
+ }
206
+ return (s) => typeof s === 'string' && s.includes(needle);
207
+ };
208
+ const hasText = (el, text) => {
209
+ const match = toMatcher(text);
210
+ return el.textContent && match(el.textContent);
211
+ };
212
+ const processSelector = (sel) => sel.replace(/>>/g, ' ');
213
+ const normalizeScoped = (sel) => {
214
+ const trimmed = sel.trim();
215
+ if (trimmed.startsWith('>') || trimmed.startsWith('+') || trimmed.startsWith('~'))
216
+ return ':scope ' + trimmed;
217
+ if (!/^:scope\\b/.test(trimmed))
218
+ return ':scope ' + trimmed;
219
+ return trimmed;
220
+ };
221
+ // multi-pseudo support for inner selectors (used by :has())
222
+ const matchSelectorWithPseudos = (root, sel) => {
223
+ // peel off known pseudos and turn them into filters
224
+ const filters = [];
225
+ // collect text/contains/has-text
226
+ sel = sel.replace(/:(?:contains|has-text|text)\\((\\"(?:[^"\\\\]|\\\\.)*\\"|'(?:[^'\\\\]|\\\\.)*'|\\\`(?:[^\\\`\\\\]|\\\\.)*\\\`|\\/(?:[^\\\\/]|\\\\.)+\\/[a-z]*)\\)/gi, (m, arg) => {
227
+ // strip quotes if present; regex stays as /.../flags
228
+ let v = arg;
229
+ const qc = v[0];
230
+ if (qc === '"' || qc === "'" || qc === '\`') v = v.slice(1, -1);
231
+ const pred = (el) => hasText(el, v);
232
+ filters.push(pred);
233
+ return '';
234
+ });
235
+ // collect :visible
236
+ sel = sel.replace(/:visible\\b/gi, () => {
237
+ filters.push((el) => isVisible(el));
238
+ return '';
239
+ });
240
+ const without = (sel.trim() || '*');
241
+ const scoped = normalizeScoped(processSelector(without));
242
+ const nodes = Array.from(root.querySelectorAll(scoped));
243
+ if (!filters.length) return nodes;
244
+ return nodes.filter(n => filters.every(f => f(n)));
245
+ };
246
+
247
+ return {
248
+ hasText: (selector, text) => Array.from(document.querySelectorAll(selector)).find(el => hasText(el, text)),
249
+ hasTextAll: (selector, text) => Array.from(document.querySelectorAll(selector)).filter(el => hasText(el, text)),
250
+ visible: (selector) => Array.from(document.querySelectorAll(selector)).find(el => isVisible(el)),
251
+ visibleAll: (selector) => Array.from(document.querySelectorAll(selector)).filter(el => isVisible(el)),
252
+ has: (baseSelector, innerSelector) => Array.from(document.querySelectorAll(baseSelector)).find(el => matchSelectorWithPseudos(el, innerSelector).length > 0),
253
+ hasAll: (baseSelector, innerSelector) => Array.from(document.querySelectorAll(baseSelector)).filter(el => matchSelectorWithPseudos(el, innerSelector).length > 0),
254
+ text: (text) => {
255
+ const match = toMatcher(text); // regex-aware
256
+ const walker = document.createTreeWalker(
257
+ document.body,
258
+ NodeFilter.SHOW_TEXT,
259
+ null
260
+ );
261
+ let node;
262
+ while (node = walker.nextNode()) {
263
+ if (node.textContent && match(node.textContent)) {
264
+ return node.parentElement;
265
+ }
266
+ }
267
+ return null;
268
+ },
269
+ // plural version (used by querySelectorAll('text=...'))
270
+ textAll: (text) => {
271
+ const match = toMatcher(text);
272
+ const out = [];
273
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
274
+ let node;
275
+ while (node = walker.nextNode()) {
276
+ if (node.textContent && match(node.textContent) && node.parentElement) {
277
+ out.push(node.parentElement);
278
+ }
279
+ }
280
+ return out;
281
+ },
282
+ // nth-match helpers
283
+ nthMatch: (baseSelector, innerSelector, indexStr) => {
284
+ const idx = Math.max(1, parseInt(indexStr, 10) || 1) - 1;
285
+ const bases = (baseSelector && baseSelector.trim())
286
+ ? Array.from(document.querySelectorAll(baseSelector))
287
+ : [document];
288
+ const all = [];
289
+ for (const b of bases) {
290
+ const nodes = matchSelectorWithPseudos(b, innerSelector);
291
+ for (const n of nodes) all.push(n);
292
+ }
293
+ return all[idx] || null;
294
+ },
295
+ nthMatchAll: (baseSelector, innerSelector, indexStr) => {
296
+ const el = window.__pwHelpers.nthMatch(baseSelector, innerSelector, indexStr);
297
+ return el ? [el] : [];
298
+ },
299
+ querySelector: (selector) => document.querySelector(processSelector(selector)),
300
+ querySelectorAll: (selector) => document.querySelectorAll(processSelector(selector))
301
+ };
302
+ })();
303
+ })();
304
+ `;
305
+ // ----- Transformers -----
306
+ export const transformHasText = ({ script, needsHelpers }) => {
307
+ if (!script.includes(":has-text("))
308
+ return { script, needsHelpers };
309
+ const result = transformSelectorCalls(script, (selector, method) => {
310
+ const parsed = parseFirstPseudo(selector);
311
+ if (parsed.kind !== "hasText")
312
+ return null;
313
+ // if base is empty, behave like :text() — search globally
314
+ if (isBlank(parsed.base)) {
315
+ return method === "all"
316
+ ? createHelperCall("hasTextAll", "*", parsed.text)
317
+ : createHelperCall("text", parsed.text);
318
+ }
319
+ return method === "all"
320
+ ? createHelperCall("hasTextAll", parsed.base, parsed.text)
321
+ : createHelperCall("hasText", parsed.base, parsed.text);
322
+ });
323
+ return {
324
+ script: result.output,
325
+ needsHelpers: needsHelpers || result.changed,
326
+ };
327
+ };
328
+ export const transformVisible = ({ script, needsHelpers }) => {
329
+ if (!script.includes(":visible"))
330
+ return { script, needsHelpers };
331
+ const result = transformSelectorCalls(script, (selector, method) => {
332
+ const parsed = parseFirstPseudo(selector);
333
+ if (parsed.kind !== "visible")
334
+ return null;
335
+ return method === "all"
336
+ ? createHelperCall("visibleAll", parsed.base)
337
+ : createHelperCall("visible", parsed.base);
338
+ });
339
+ return {
340
+ script: result.output,
341
+ needsHelpers: needsHelpers || result.changed,
342
+ };
343
+ };
344
+ export const transformText = ({ script, needsHelpers }) => {
345
+ if (!script.includes(":text("))
346
+ return { script, needsHelpers };
347
+ const result = transformSelectorCalls(script, (selector, method) => {
348
+ const parsed = parseFirstPseudo(selector);
349
+ if (parsed.kind !== "text")
350
+ return null;
351
+ if (isBlank(parsed.base)) {
352
+ return method === "all"
353
+ ? createHelperCall("hasTextAll", "*", parsed.text)
354
+ : createHelperCall("text", parsed.text);
355
+ }
356
+ return method === "all"
357
+ ? createHelperCall("hasTextAll", parsed.base, parsed.text)
358
+ : createHelperCall("hasText", parsed.base, parsed.text);
359
+ });
360
+ return {
361
+ script: result.output,
362
+ needsHelpers: needsHelpers || result.changed,
363
+ };
364
+ };
365
+ // Support playwright "text=..." engine (and "X >> text=...")
366
+ export const transformTextEngine = ({ script, needsHelpers }) => {
367
+ const result = transformSelectorCalls(script, (selector, method) => {
368
+ const raw = selector.trim();
369
+ // Case 1: entire selector is text=...
370
+ const m1 = raw.match(/^\s*text\s*=\s*(.+)\s*$/);
371
+ if (m1) {
372
+ const arg = m1[1].trim();
373
+ const val = unwrapPossibleQuotesOrKeepRegex(arg);
374
+ return method === "all"
375
+ ? createHelperCall("textAll", val)
376
+ : createHelperCall("text", val);
377
+ }
378
+ // Case 2: X >> text=...
379
+ const m2 = raw.match(/^(.*?)>>\s*text\s*=\s*(.+)\s*$/);
380
+ if (m2) {
381
+ const base = m2[1].trim() || "*";
382
+ const arg = m2[2].trim();
383
+ const val = unwrapPossibleQuotesOrKeepRegex(arg);
384
+ return method === "all"
385
+ ? createHelperCall("hasTextAll", base, val)
386
+ : createHelperCall("hasText", base, val);
387
+ }
388
+ return null;
389
+ });
390
+ return {
391
+ script: result.output,
392
+ needsHelpers: needsHelpers || result.changed,
393
+ };
394
+ };
395
+ // Utility used only by transformTextEngine
396
+ const unwrapPossibleQuotesOrKeepRegex = (s) => {
397
+ const q = s[0];
398
+ if ((q === '"' || q === "'" || q === "`") &&
399
+ s.length >= 2 &&
400
+ s[s.length - 1] === q)
401
+ return s.slice(1, -1);
402
+ // keep /.../flags as-is
403
+ return s;
404
+ };
405
+ export const transformContains = ({ script, needsHelpers }) => {
406
+ if (!script.includes(":contains("))
407
+ return { script, needsHelpers };
408
+ const result = transformSelectorCalls(script, (selector, method) => {
409
+ const parsed = parseFirstPseudo(selector);
410
+ if (parsed.kind !== "contains")
411
+ return null;
412
+ return method === "all"
413
+ ? createHelperCall("hasTextAll", parsed.base, parsed.text)
414
+ : createHelperCall("hasText", parsed.base, parsed.text);
415
+ });
416
+ return {
417
+ script: result.output,
418
+ needsHelpers: needsHelpers || result.changed,
419
+ };
420
+ };
421
+ export const transformHas = ({ script, needsHelpers }) => {
422
+ if (!script.includes(":has("))
423
+ return { script, needsHelpers };
424
+ const result = transformSelectorCalls(script, (selector, method) => {
425
+ const parsed = parseFirstPseudo(selector);
426
+ if (parsed.kind !== "has")
427
+ return null;
428
+ return method === "all"
429
+ ? createHelperCall("hasAll", parsed.base, parsed.inner)
430
+ : createHelperCall("has", parsed.base, parsed.inner);
431
+ });
432
+ return {
433
+ script: result.output,
434
+ needsHelpers: needsHelpers || result.changed,
435
+ };
436
+ };
437
+ // :nth-match(...)
438
+ export const transformNthMatch = ({ script, needsHelpers }) => {
439
+ if (!script.includes(":nth-match("))
440
+ return { script, needsHelpers };
441
+ const result = transformSelectorCalls(script, (selector, method) => {
442
+ const parsed = parseFirstPseudo(selector);
443
+ if (parsed.kind !== "nthMatch")
444
+ return null;
445
+ // If base is empty, treat as global
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));
450
+ });
451
+ return {
452
+ script: result.output,
453
+ needsHelpers: needsHelpers || result.changed,
454
+ };
455
+ };
456
+ export const transformDescendant = ({ script, needsHelpers }) => {
457
+ if (!script.includes(">>"))
458
+ return { script, needsHelpers };
459
+ const result = transformSelectorCalls(script, (selector, method) => {
460
+ if (!selector.includes(">>"))
461
+ return null;
462
+ return method === "all"
463
+ ? createHelperCall("querySelectorAll", selector)
464
+ : createHelperCall("querySelector", selector);
465
+ });
466
+ return {
467
+ script: result.output,
468
+ needsHelpers: needsHelpers || result.changed,
469
+ };
470
+ };
471
+ // Inject helpers if needed
472
+ export const injectHelpers = ({ script, needsHelpers }) => needsHelpers
473
+ ? {
474
+ script: helperFunctions +
475
+ "\n/* Playwright selectors automatically transformed */\n" +
476
+ script,
477
+ needsHelpers,
478
+ }
479
+ : { script, needsHelpers };
480
+ // Main transformation pipeline
481
+ export const transformPlaywrightSyntax = (script) => pipe({ script, needsHelpers: false }, transformHas, transformHasText, transformVisible, transformText, transformContains, transformNthMatch, transformTextEngine, transformDescendant, injectHelpers);
482
+ // Memoized version for performance
483
+ export const memoizedTransform = memoize(transformPlaywrightSyntax);
484
+ // Public API
485
+ export const transformScript = (originalScript) => {
486
+ const result = memoizedTransform(originalScript);
487
+ return result.script;
488
+ };
489
+ // Testing utilities
490
+ export const testTransformation = (input, expected) => {
491
+ const { script } = transformPlaywrightSyntax(input);
492
+ return script === expected;
493
+ };
494
+ const transformSelectorCalls = (code, rewriter) => {
495
+ const start = skipLexer.parse(code);
496
+ if (!start)
497
+ return { output: code, changed: false };
498
+ const parsed = PROGRAM.parse(start);
499
+ if (!parsed.successful || parsed.candidates.length === 0)
500
+ return { output: code, changed: false };
501
+ const segments = parsed.candidates[0].result;
502
+ let changed = false;
503
+ const out = [];
504
+ for (const seg of segments) {
505
+ if (seg.kind === "call") {
506
+ const replacement = rewriter(seg.selector, seg.method);
507
+ if (replacement) {
508
+ out.push(replacement);
509
+ changed = true;
510
+ }
511
+ else {
512
+ out.push(seg.original);
513
+ }
514
+ }
515
+ else {
516
+ out.push(seg.text);
517
+ }
518
+ }
519
+ return { output: out.join(""), changed };
520
+ };
package/lib/program.js ADDED
@@ -0,0 +1,112 @@
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 { 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
+ program
28
+ .version("Version " + packageJSON.version)
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())
63
+ .action(async (options) => {
64
+ setupExitWatchdog();
65
+ if (options.vision) {
66
+ // eslint-disable-next-line no-console
67
+ console.error("The --vision option is deprecated, use --caps=vision instead");
68
+ options.caps = "vision";
69
+ }
70
+ const config = await resolveCLIConfig(options);
71
+ if (options.extension) {
72
+ const contextFactory = createExtensionContextFactory(config);
73
+ const serverBackendFactory = () => new BrowserServerBackend(config, contextFactory);
74
+ await mcpTransport.start(serverBackendFactory, config.server);
75
+ return;
76
+ }
77
+ const browserContextFactory = contextFactory(config);
78
+ const providers = [
79
+ mcpProviderForBrowserContextFactory(config, browserContextFactory),
80
+ ];
81
+ if (options.connectTool)
82
+ providers.push(mcpProviderForBrowserContextFactory(config, createExtensionContextFactory(config)));
83
+ await mcpTransport.start(() => new ProxyBackend(providers), config.server);
84
+ });
85
+ function setupExitWatchdog() {
86
+ let isExiting = false;
87
+ const handleExit = async () => {
88
+ if (isExiting)
89
+ return;
90
+ isExiting = true;
91
+ setTimeout(() => process.exit(0), 15000);
92
+ await Context.disposeAll();
93
+ process.exit(0);
94
+ };
95
+ process.stdin.on("close", handleExit);
96
+ process.on("SIGINT", handleExit);
97
+ process.on("SIGTERM", handleExit);
98
+ }
99
+ function createExtensionContextFactory(config) {
100
+ return new ExtensionContextFactory(config.browser.launchOptions.channel || "chrome", config.browser.userDataDir);
101
+ }
102
+ function mcpProviderForBrowserContextFactory(config, browserContextFactory) {
103
+ return {
104
+ name: browserContextFactory.name,
105
+ description: browserContextFactory.description,
106
+ connect: async () => {
107
+ const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false);
108
+ return new InProcessTransport(server);
109
+ },
110
+ };
111
+ }
112
+ void program.parseAsync(process.argv);