@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.
- package/lib/browserContextFactory.js +399 -0
- package/lib/browserServerBackend.js +86 -0
- package/lib/config.js +300 -0
- package/lib/context.js +311 -0
- package/lib/extension/cdpRelay.js +352 -0
- package/lib/extension/extensionContextFactory.js +56 -0
- package/lib/frameworkPatterns.js +35 -0
- package/lib/hooks/antiBotDetectionHook.js +178 -0
- package/lib/hooks/core.js +145 -0
- package/lib/hooks/eventConsumer.js +52 -0
- package/lib/hooks/events.js +42 -0
- package/lib/hooks/formatToolCallEvent.js +12 -0
- package/lib/hooks/frameworkStateHook.js +182 -0
- package/lib/hooks/grouping.js +72 -0
- package/lib/hooks/jsonLdDetectionHook.js +182 -0
- package/lib/hooks/networkFilters.js +82 -0
- package/lib/hooks/networkSetup.js +61 -0
- package/lib/hooks/networkTrackingHook.js +67 -0
- package/lib/hooks/pageHeightHook.js +75 -0
- package/lib/hooks/registry.js +41 -0
- package/lib/hooks/requireTabHook.js +26 -0
- package/lib/hooks/schema.js +89 -0
- package/lib/hooks/waitHook.js +33 -0
- package/lib/index.js +41 -0
- package/lib/mcp/inProcessTransport.js +71 -0
- package/lib/mcp/proxyBackend.js +130 -0
- package/lib/mcp/server.js +91 -0
- package/lib/mcp/tool.js +44 -0
- package/lib/mcp/transport.js +188 -0
- package/lib/playwrightTransformer.js +520 -0
- package/lib/program.js +112 -0
- package/lib/response.js +192 -0
- package/lib/sessionLog.js +123 -0
- package/lib/tab.js +251 -0
- package/lib/tools/common.js +55 -0
- package/lib/tools/console.js +33 -0
- package/lib/tools/dialogs.js +50 -0
- package/lib/tools/evaluate.js +62 -0
- package/lib/tools/extractFrameworkState.js +225 -0
- package/lib/tools/files.js +48 -0
- package/lib/tools/form.js +66 -0
- package/lib/tools/getSnapshot.js +36 -0
- package/lib/tools/getVisibleHtml.js +68 -0
- package/lib/tools/install.js +51 -0
- package/lib/tools/keyboard.js +83 -0
- package/lib/tools/mouse.js +97 -0
- package/lib/tools/navigate.js +66 -0
- package/lib/tools/network.js +121 -0
- package/lib/tools/networkDetail.js +238 -0
- package/lib/tools/networkSearch/bodySearch.js +161 -0
- package/lib/tools/networkSearch/grouping.js +37 -0
- package/lib/tools/networkSearch/helpers.js +32 -0
- package/lib/tools/networkSearch/searchHtml.js +76 -0
- package/lib/tools/networkSearch/types.js +1 -0
- package/lib/tools/networkSearch/urlSearch.js +124 -0
- package/lib/tools/networkSearch.js +278 -0
- package/lib/tools/pdf.js +41 -0
- package/lib/tools/repl.js +414 -0
- package/lib/tools/screenshot.js +103 -0
- package/lib/tools/scroll.js +131 -0
- package/lib/tools/snapshot.js +161 -0
- package/lib/tools/tabs.js +62 -0
- package/lib/tools/tool.js +35 -0
- package/lib/tools/utils.js +78 -0
- package/lib/tools/wait.js +60 -0
- package/lib/tools.js +68 -0
- package/lib/utils/adBlockFilter.js +90 -0
- package/lib/utils/codegen.js +55 -0
- package/lib/utils/extensionPath.js +10 -0
- package/lib/utils/fileUtils.js +40 -0
- package/lib/utils/graphql.js +269 -0
- package/lib/utils/guid.js +22 -0
- package/lib/utils/httpServer.js +39 -0
- package/lib/utils/log.js +21 -0
- package/lib/utils/manualPromise.js +111 -0
- package/lib/utils/networkFormat.js +14 -0
- package/lib/utils/package.js +20 -0
- package/lib/utils/result.js +2 -0
- package/lib/utils/sanitizeHtml.js +130 -0
- package/lib/utils/truncate.js +103 -0
- package/lib/utils/withTimeout.js +7 -0
- 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);
|