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