@wordbricks/playwright-mcp 0.1.19 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -44
- 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 +178 -0
- package/lib/hooks/core.js +11 -10
- package/lib/hooks/eventConsumer.js +29 -16
- 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 +24 -15
- package/lib/hooks/networkSetup.js +11 -6
- package/lib/hooks/networkTrackingHook.js +31 -19
- package/lib/hooks/pageHeightHook.js +9 -9
- package/lib/hooks/registry.js +18 -16
- package/lib/hooks/requireTabHook.js +3 -3
- package/lib/hooks/schema.js +44 -32
- 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 +68 -61
- 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 +65 -35
- 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
package/lib/tools/keyboard.js
CHANGED
|
@@ -13,21 +13,23 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { z } from
|
|
17
|
-
import
|
|
18
|
-
import { elementSchema } from
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import * as javascript from "../utils/codegen.js";
|
|
18
|
+
import { elementSchema } from "./snapshot.js";
|
|
19
|
+
import { defineTabTool } from "./tool.js";
|
|
20
|
+
import { generateLocator } from "./utils.js";
|
|
21
21
|
const pressKey = defineTabTool({
|
|
22
|
-
capability:
|
|
22
|
+
capability: "core",
|
|
23
23
|
schema: {
|
|
24
|
-
name:
|
|
25
|
-
title:
|
|
26
|
-
description:
|
|
24
|
+
name: "browser_press_key",
|
|
25
|
+
title: "Press a key",
|
|
26
|
+
description: "Press a key on the keyboard",
|
|
27
27
|
inputSchema: z.object({
|
|
28
|
-
key: z
|
|
28
|
+
key: z
|
|
29
|
+
.string()
|
|
30
|
+
.describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`"),
|
|
29
31
|
}),
|
|
30
|
-
type:
|
|
32
|
+
type: "input",
|
|
31
33
|
},
|
|
32
34
|
handle: async (tab, params, response) => {
|
|
33
35
|
response.setIncludeSnapshot();
|
|
@@ -39,18 +41,24 @@ const pressKey = defineTabTool({
|
|
|
39
41
|
},
|
|
40
42
|
});
|
|
41
43
|
const typeSchema = elementSchema.extend({
|
|
42
|
-
text: z.string().describe(
|
|
43
|
-
submit: z
|
|
44
|
-
|
|
44
|
+
text: z.string().describe("Text to type into the element"),
|
|
45
|
+
submit: z
|
|
46
|
+
.boolean()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Whether to submit entered text (press Enter after)"),
|
|
49
|
+
slowly: z
|
|
50
|
+
.boolean()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once."),
|
|
45
53
|
});
|
|
46
54
|
const type = defineTabTool({
|
|
47
|
-
capability:
|
|
55
|
+
capability: "core",
|
|
48
56
|
schema: {
|
|
49
|
-
name:
|
|
50
|
-
title:
|
|
51
|
-
description:
|
|
57
|
+
name: "browser_type",
|
|
58
|
+
title: "Type text",
|
|
59
|
+
description: "Type text into editable element",
|
|
52
60
|
inputSchema: typeSchema,
|
|
53
|
-
type:
|
|
61
|
+
type: "input",
|
|
54
62
|
},
|
|
55
63
|
handle: async (tab, params, response) => {
|
|
56
64
|
const locator = await tab.refLocator(params);
|
|
@@ -67,12 +75,9 @@ const type = defineTabTool({
|
|
|
67
75
|
if (params.submit) {
|
|
68
76
|
response.setIncludeSnapshot();
|
|
69
77
|
response.addCode(`await page.${await generateLocator(locator)}.press('Enter');`);
|
|
70
|
-
await locator.press(
|
|
78
|
+
await locator.press("Enter");
|
|
71
79
|
}
|
|
72
80
|
});
|
|
73
81
|
},
|
|
74
82
|
});
|
|
75
|
-
export default [
|
|
76
|
-
pressKey,
|
|
77
|
-
type,
|
|
78
|
-
];
|
|
83
|
+
export default [pressKey, type];
|
package/lib/tools/mouse.js
CHANGED
|
@@ -13,22 +13,24 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { z } from
|
|
17
|
-
import { defineTabTool } from
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { defineTabTool } from "./tool.js";
|
|
18
18
|
const elementSchema = z.object({
|
|
19
|
-
element: z
|
|
19
|
+
element: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe("Human-readable element description used to obtain permission to interact with the element"),
|
|
20
22
|
});
|
|
21
23
|
const mouseMove = defineTabTool({
|
|
22
|
-
capability:
|
|
24
|
+
capability: "vision",
|
|
23
25
|
schema: {
|
|
24
|
-
name:
|
|
25
|
-
title:
|
|
26
|
-
description:
|
|
26
|
+
name: "browser_mouse_move_xy",
|
|
27
|
+
title: "Move mouse",
|
|
28
|
+
description: "Move mouse to a given position",
|
|
27
29
|
inputSchema: elementSchema.extend({
|
|
28
|
-
x: z.number().describe(
|
|
29
|
-
y: z.number().describe(
|
|
30
|
+
x: z.number().describe("X coordinate"),
|
|
31
|
+
y: z.number().describe("Y coordinate"),
|
|
30
32
|
}),
|
|
31
|
-
type:
|
|
33
|
+
type: "readOnly",
|
|
32
34
|
},
|
|
33
35
|
handle: async (tab, params, response) => {
|
|
34
36
|
response.addCode(`// Move mouse to (${params.x}, ${params.y})`);
|
|
@@ -39,16 +41,16 @@ const mouseMove = defineTabTool({
|
|
|
39
41
|
},
|
|
40
42
|
});
|
|
41
43
|
const mouseClick = defineTabTool({
|
|
42
|
-
capability:
|
|
44
|
+
capability: "vision",
|
|
43
45
|
schema: {
|
|
44
|
-
name:
|
|
45
|
-
title:
|
|
46
|
-
description:
|
|
46
|
+
name: "browser_mouse_click_xy",
|
|
47
|
+
title: "Click",
|
|
48
|
+
description: "Click left mouse button at a given position",
|
|
47
49
|
inputSchema: elementSchema.extend({
|
|
48
|
-
x: z.number().describe(
|
|
49
|
-
y: z.number().describe(
|
|
50
|
+
x: z.number().describe("X coordinate"),
|
|
51
|
+
y: z.number().describe("Y coordinate"),
|
|
50
52
|
}),
|
|
51
|
-
type:
|
|
53
|
+
type: "input",
|
|
52
54
|
},
|
|
53
55
|
handle: async (tab, params, response) => {
|
|
54
56
|
response.setIncludeSnapshot();
|
|
@@ -64,18 +66,18 @@ const mouseClick = defineTabTool({
|
|
|
64
66
|
},
|
|
65
67
|
});
|
|
66
68
|
const mouseDrag = defineTabTool({
|
|
67
|
-
capability:
|
|
69
|
+
capability: "vision",
|
|
68
70
|
schema: {
|
|
69
|
-
name:
|
|
70
|
-
title:
|
|
71
|
-
description:
|
|
71
|
+
name: "browser_mouse_drag_xy",
|
|
72
|
+
title: "Drag mouse",
|
|
73
|
+
description: "Drag left mouse button to a given position",
|
|
72
74
|
inputSchema: elementSchema.extend({
|
|
73
|
-
startX: z.number().describe(
|
|
74
|
-
startY: z.number().describe(
|
|
75
|
-
endX: z.number().describe(
|
|
76
|
-
endY: z.number().describe(
|
|
75
|
+
startX: z.number().describe("Start X coordinate"),
|
|
76
|
+
startY: z.number().describe("Start Y coordinate"),
|
|
77
|
+
endX: z.number().describe("End X coordinate"),
|
|
78
|
+
endY: z.number().describe("End Y coordinate"),
|
|
77
79
|
}),
|
|
78
|
-
type:
|
|
80
|
+
type: "input",
|
|
79
81
|
},
|
|
80
82
|
handle: async (tab, params, response) => {
|
|
81
83
|
response.setIncludeSnapshot();
|
|
@@ -92,8 +94,4 @@ const mouseDrag = defineTabTool({
|
|
|
92
94
|
});
|
|
93
95
|
},
|
|
94
96
|
});
|
|
95
|
-
export default [
|
|
96
|
-
mouseMove,
|
|
97
|
-
mouseClick,
|
|
98
|
-
mouseDrag,
|
|
99
|
-
];
|
|
97
|
+
export default [mouseMove, mouseClick, mouseDrag];
|
package/lib/tools/navigate.js
CHANGED
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { z } from
|
|
17
|
-
import {
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { defineTabTool, defineTool } from "./tool.js";
|
|
18
18
|
const navigate = defineTool({
|
|
19
|
-
capability:
|
|
19
|
+
capability: "core",
|
|
20
20
|
schema: {
|
|
21
|
-
name:
|
|
22
|
-
title:
|
|
23
|
-
description:
|
|
21
|
+
name: "browser_navigate",
|
|
22
|
+
title: "Navigate to a URL",
|
|
23
|
+
description: "Navigate to a URL",
|
|
24
24
|
inputSchema: z.object({
|
|
25
|
-
url: z.string().describe(
|
|
25
|
+
url: z.string().describe("The URL to navigate to"),
|
|
26
26
|
}),
|
|
27
|
-
type:
|
|
27
|
+
type: "action",
|
|
28
28
|
},
|
|
29
29
|
handle: async (context, params, response) => {
|
|
30
30
|
const tab = await context.ensureTab();
|
|
@@ -34,13 +34,13 @@ const navigate = defineTool({
|
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
const goBack = defineTabTool({
|
|
37
|
-
capability:
|
|
37
|
+
capability: "core",
|
|
38
38
|
schema: {
|
|
39
|
-
name:
|
|
40
|
-
title:
|
|
41
|
-
description:
|
|
39
|
+
name: "browser_navigate_back",
|
|
40
|
+
title: "Go back",
|
|
41
|
+
description: "Go back to the previous page",
|
|
42
42
|
inputSchema: z.object({}),
|
|
43
|
-
type:
|
|
43
|
+
type: "readOnly",
|
|
44
44
|
},
|
|
45
45
|
handle: async (tab, params, response) => {
|
|
46
46
|
await tab.page.goBack();
|
|
@@ -49,13 +49,13 @@ const goBack = defineTabTool({
|
|
|
49
49
|
},
|
|
50
50
|
});
|
|
51
51
|
const goForward = defineTabTool({
|
|
52
|
-
capability:
|
|
52
|
+
capability: "core",
|
|
53
53
|
schema: {
|
|
54
|
-
name:
|
|
55
|
-
title:
|
|
56
|
-
description:
|
|
54
|
+
name: "browser_navigate_forward",
|
|
55
|
+
title: "Go forward",
|
|
56
|
+
description: "Go forward to the next page",
|
|
57
57
|
inputSchema: z.object({}),
|
|
58
|
-
type:
|
|
58
|
+
type: "readOnly",
|
|
59
59
|
},
|
|
60
60
|
handle: async (tab, params, response) => {
|
|
61
61
|
await tab.page.goForward();
|
|
@@ -63,8 +63,4 @@ const goForward = defineTabTool({
|
|
|
63
63
|
response.addCode(`await page.goForward();`);
|
|
64
64
|
},
|
|
65
65
|
});
|
|
66
|
-
export default [
|
|
67
|
-
navigate,
|
|
68
|
-
goBack,
|
|
69
|
-
goForward,
|
|
70
|
-
];
|
|
66
|
+
export default [navigate, goBack, goForward];
|
package/lib/tools/network.js
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { z } from
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { formatUrlWithTrimmedParams } from "../hooks/networkFilters.js";
|
|
18
|
+
import { defineTabTool } from "./tool.js";
|
|
19
19
|
// const requests = defineTool({
|
|
20
20
|
// capability: 'core',
|
|
21
21
|
// schema: {
|
|
@@ -61,13 +61,13 @@ import { formatUrlWithTrimmedParams } from '../hooks/networkFilters.js';
|
|
|
61
61
|
// },
|
|
62
62
|
// });
|
|
63
63
|
const requests = defineTabTool({
|
|
64
|
-
capability:
|
|
64
|
+
capability: "core",
|
|
65
65
|
schema: {
|
|
66
|
-
name:
|
|
67
|
-
title:
|
|
68
|
-
description:
|
|
66
|
+
name: "browser_network_requests",
|
|
67
|
+
title: "List network requests",
|
|
68
|
+
description: "Returns all network requests since loading the page",
|
|
69
69
|
inputSchema: z.object({}),
|
|
70
|
-
type:
|
|
70
|
+
type: "readOnly",
|
|
71
71
|
},
|
|
72
72
|
handle: async (tab, params, response) => {
|
|
73
73
|
const requests = tab.requests();
|
|
@@ -82,8 +82,8 @@ export async function renderRequest(request, response) {
|
|
|
82
82
|
result.push(`[${request.method().toUpperCase()}] ${trimmedUrl}`);
|
|
83
83
|
// Append response status if available with compact content-type
|
|
84
84
|
if (response) {
|
|
85
|
-
const ct = response.headers()?.[
|
|
86
|
-
result.push(`=> [${response.status()}] ${response.statusText()}${ct ? ` ct=${ct}` :
|
|
85
|
+
const ct = response.headers()?.["content-type"];
|
|
86
|
+
result.push(`=> [${response.status()}] ${response.statusText()}${ct ? ` ct=${ct}` : ""}`);
|
|
87
87
|
}
|
|
88
88
|
// Always include request headers (excluding HTTP/2 pseudo-headers and common headers)
|
|
89
89
|
// const allHeaders = await request.allHeaders();
|
|
@@ -116,8 +116,6 @@ export async function renderRequest(request, response) {
|
|
|
116
116
|
// if (body)
|
|
117
117
|
// result.push(`body=${body}`);
|
|
118
118
|
// }
|
|
119
|
-
return result.join(
|
|
119
|
+
return result.join("\n");
|
|
120
120
|
}
|
|
121
|
-
export default [
|
|
122
|
-
requests,
|
|
123
|
-
];
|
|
121
|
+
export default [requests];
|
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
const TIMEOUT_MS = ms('5s');
|
|
1
|
+
import ms from "ms";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getNetworkEventEntry } from "../hooks/networkSetup.js";
|
|
4
|
+
import { extractGraphQLResponseInfo, minifyGraphQLQuery, minifyGraphQLRequestBody, parseGraphQLRequestFromHttp, summarizeGraphQL, } from "../utils/graphql.js";
|
|
5
|
+
import { formatNetworkSummaryLine } from "../utils/networkFormat.js";
|
|
6
|
+
import { sanitizeHtml } from "../utils/sanitizeHtml.js";
|
|
7
|
+
import { formatTruncationLine } from "../utils/truncate.js";
|
|
8
|
+
import { withTimeout } from "../utils/withTimeout.js";
|
|
9
|
+
import { defineTabTool } from "./tool.js";
|
|
10
|
+
const TIMEOUT_MS = ms("5s");
|
|
12
11
|
const CHUNK_BYTES = 8 * 1024; // 8KB per page
|
|
13
12
|
const contextBodyMap = new WeakMap();
|
|
14
13
|
// Skip HTTP/2 pseudo-headers and their non-colon variants in displayed request headers
|
|
15
14
|
const SKIP_REQUEST_HEADER_KEYS = new Set([
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
":authority",
|
|
16
|
+
":method",
|
|
17
|
+
":path",
|
|
18
|
+
":scheme",
|
|
19
|
+
"authority",
|
|
20
|
+
"method",
|
|
21
|
+
"path",
|
|
22
|
+
"scheme",
|
|
24
23
|
]);
|
|
25
24
|
const getStateMap = (ctx) => {
|
|
26
25
|
let m = contextBodyMap.get(ctx);
|
|
@@ -31,7 +30,7 @@ const getStateMap = (ctx) => {
|
|
|
31
30
|
return m;
|
|
32
31
|
};
|
|
33
32
|
const networkDetailSchema = z.object({
|
|
34
|
-
id: z.number().describe(
|
|
33
|
+
id: z.number().describe("The network event id from events log"),
|
|
35
34
|
});
|
|
36
35
|
const normalizeHeaderKeys = (headers) => {
|
|
37
36
|
const out = {};
|
|
@@ -40,7 +39,7 @@ const normalizeHeaderKeys = (headers) => {
|
|
|
40
39
|
return out;
|
|
41
40
|
};
|
|
42
41
|
const formatBytes = (n) => {
|
|
43
|
-
const units = [
|
|
42
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
44
43
|
let i = 0;
|
|
45
44
|
let v = n;
|
|
46
45
|
while (v >= 1024 && i < units.length - 1) {
|
|
@@ -68,16 +67,16 @@ const formatRequestDetail = async (ctx, { request, response, id, }) => {
|
|
|
68
67
|
if (!Number.isNaN(duration))
|
|
69
68
|
lines.push(`Duration: ${duration}ms`);
|
|
70
69
|
}
|
|
71
|
-
lines.push(`Failure: ${request.failure()?.errorText ||
|
|
70
|
+
lines.push(`Failure: ${request.failure()?.errorText || "None"}`);
|
|
72
71
|
if (!response) {
|
|
73
72
|
lines.push(`\nRequest Headers:`);
|
|
74
73
|
const headerKeysNoResp = Object.keys(reqHeaders)
|
|
75
|
-
.filter(k => !SKIP_REQUEST_HEADER_KEYS.has(k))
|
|
74
|
+
.filter((k) => !SKIP_REQUEST_HEADER_KEYS.has(k))
|
|
76
75
|
.sort();
|
|
77
76
|
for (const key of headerKeysNoResp)
|
|
78
77
|
lines.push(`${key}: ${reqHeaders[key]}`);
|
|
79
78
|
lines.push(`\nNo response received yet`);
|
|
80
|
-
return lines.join(
|
|
79
|
+
return lines.join("\n");
|
|
81
80
|
}
|
|
82
81
|
// Determine if this request is GraphQL (used for both request and response sections)
|
|
83
82
|
const gqlReq = parseGraphQLRequestFromHttp(request.method(), request.url(), reqHeaders, requestBody);
|
|
@@ -97,25 +96,27 @@ const formatRequestDetail = async (ctx, { request, response, id, }) => {
|
|
|
97
96
|
// Print full request headers on the first call (excluding authority/method/path/scheme)
|
|
98
97
|
lines.push(`\nRequest Headers:`);
|
|
99
98
|
const headerKeys = Object.keys(reqHeaders)
|
|
100
|
-
.filter(k => !SKIP_REQUEST_HEADER_KEYS.has(k))
|
|
99
|
+
.filter((k) => !SKIP_REQUEST_HEADER_KEYS.has(k))
|
|
101
100
|
.sort();
|
|
102
101
|
for (const key of headerKeys)
|
|
103
102
|
lines.push(`${key}: ${reqHeaders[key]}`);
|
|
104
|
-
const reqCt = reqHeaders[
|
|
103
|
+
const reqCt = reqHeaders["content-type"];
|
|
105
104
|
// GraphQL request summary (if applicable) and compact body handling
|
|
106
105
|
if (gqlReq) {
|
|
107
106
|
lines.push(`\nGraphQL Request: ${summarizeGraphQL(gqlReq)}`);
|
|
108
|
-
if (gqlReq.variables &&
|
|
107
|
+
if (gqlReq.variables &&
|
|
108
|
+
typeof gqlReq.variables === "object" &&
|
|
109
|
+
!Array.isArray(gqlReq.variables)) {
|
|
109
110
|
const varKeys = Object.keys(gqlReq.variables);
|
|
110
111
|
if (varKeys.length)
|
|
111
|
-
lines.push(`Variables: { ${varKeys.join(
|
|
112
|
+
lines.push(`Variables: { ${varKeys.join(", ")} }`);
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
if (requestBody) {
|
|
115
116
|
if (gqlReq) {
|
|
116
117
|
lines.push(`\nRequest Body (GraphQL):`);
|
|
117
118
|
try {
|
|
118
|
-
if (reqCt && reqCt.includes(
|
|
119
|
+
if (reqCt && reqCt.includes("application/graphql")) {
|
|
119
120
|
lines.push(minifyGraphQLQuery(requestBody));
|
|
120
121
|
}
|
|
121
122
|
else {
|
|
@@ -139,15 +140,23 @@ const formatRequestDetail = async (ctx, { request, response, id, }) => {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
const resHeaders = normalizeHeaderKeys(await response.allHeaders());
|
|
142
|
-
const resCt = resHeaders[
|
|
143
|
-
const resCl = resHeaders[
|
|
143
|
+
const resCt = resHeaders["content-type"];
|
|
144
|
+
const resCl = resHeaders["content-length"];
|
|
144
145
|
const headerParts = [];
|
|
145
146
|
if (resCt)
|
|
146
147
|
headerParts.push(`content-type=${resCt}`);
|
|
147
148
|
if (resCl)
|
|
148
149
|
headerParts.push(`content-length=${resCl}`);
|
|
149
150
|
if (headerParts.length)
|
|
150
|
-
lines.push(`\nResponse Headers: { ${headerParts.join(
|
|
151
|
+
lines.push(`\nResponse Headers: { ${headerParts.join(", ")} }`);
|
|
152
|
+
const setCookieHeaders = await response
|
|
153
|
+
.headerValues("set-cookie")
|
|
154
|
+
.catch(() => []);
|
|
155
|
+
if (setCookieHeaders.length) {
|
|
156
|
+
lines.push(`\nSet-Cookie Headers (${setCookieHeaders.length}):`);
|
|
157
|
+
for (const cookie of setCookieHeaders)
|
|
158
|
+
lines.push(cookie);
|
|
159
|
+
}
|
|
151
160
|
}
|
|
152
161
|
// Fetch the full body each time
|
|
153
162
|
let buf;
|
|
@@ -157,63 +166,61 @@ const formatRequestDetail = async (ctx, { request, response, id, }) => {
|
|
|
157
166
|
catch (e) {
|
|
158
167
|
const msg = e instanceof Error ? e.message : String(e);
|
|
159
168
|
lines.push(`\nFailed to read response body: ${msg}`);
|
|
160
|
-
return lines.join(
|
|
169
|
+
return lines.join("\n");
|
|
161
170
|
}
|
|
162
171
|
// On first page, summarize GraphQL response if applicable
|
|
163
172
|
if (!state.headersShown && state.isGraphQLRequest) {
|
|
164
173
|
try {
|
|
165
|
-
const fullText = buf.toString(
|
|
174
|
+
const fullText = buf.toString("utf8");
|
|
166
175
|
const gqlInfo = extractGraphQLResponseInfo(fullText);
|
|
167
176
|
if (gqlInfo) {
|
|
168
|
-
lines.push(`\nGraphQL Response: ${gqlInfo.hasErrors ? `${gqlInfo.errorCount} error(s)` :
|
|
177
|
+
lines.push(`\nGraphQL Response: ${gqlInfo.hasErrors ? `${gqlInfo.errorCount} error(s)` : "ok"}`);
|
|
169
178
|
if (gqlInfo.topMessages.length)
|
|
170
|
-
lines.push(`Errors: ${gqlInfo.topMessages.join(
|
|
179
|
+
lines.push(`Errors: ${gqlInfo.topMessages.join(" | ")}`);
|
|
171
180
|
if (gqlInfo.dataKeys.length)
|
|
172
|
-
lines.push(`Data keys: ${gqlInfo.dataKeys.join(
|
|
181
|
+
lines.push(`Data keys: ${gqlInfo.dataKeys.join(", ")}`);
|
|
173
182
|
}
|
|
174
183
|
}
|
|
175
184
|
catch { }
|
|
176
185
|
}
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
let bodyText = buf.toString("utf8");
|
|
187
|
+
const resHeadersForBody = normalizeHeaderKeys(await response.allHeaders());
|
|
188
|
+
const resCtForBody = resHeadersForBody["content-type"] || "";
|
|
189
|
+
const isHtml = resCtForBody.includes("html");
|
|
190
|
+
if (isHtml)
|
|
191
|
+
bodyText = sanitizeHtml(bodyText, {
|
|
192
|
+
shouldRemoveScripts: false,
|
|
193
|
+
shouldRemoveStyles: true,
|
|
194
|
+
});
|
|
195
|
+
const totalLength = bodyText.length;
|
|
196
|
+
if (state.cursor >= totalLength) {
|
|
179
197
|
state.cursor = 0;
|
|
180
198
|
state.headersShown = false; // allow headers again after a full pass
|
|
181
199
|
}
|
|
182
|
-
let bodyText = buf.toString('utf8');
|
|
183
|
-
const resHeadersForBody = normalizeHeaderKeys(await response.allHeaders());
|
|
184
|
-
const resCtForBody = resHeadersForBody['content-type'] || '';
|
|
185
|
-
const isHtml = resCtForBody.includes('html');
|
|
186
|
-
if (isHtml) {
|
|
187
|
-
const $ = cheerio.load(bodyText, { xmlMode: false });
|
|
188
|
-
removeNonEssentialLinks($);
|
|
189
|
-
removeMetaTags($);
|
|
190
|
-
removeHtmlComments($);
|
|
191
|
-
stripSvgAttributes($);
|
|
192
|
-
bodyText = minifyHtml($.root().html() || '');
|
|
193
|
-
}
|
|
194
|
-
const totalLength = bodyText.length;
|
|
195
200
|
const start = state.cursor;
|
|
196
201
|
const end = Math.min(start + CHUNK_BYTES, totalLength);
|
|
197
202
|
const chunk = bodyText.slice(start, end);
|
|
198
203
|
const nextCursor = end;
|
|
199
204
|
const more = nextCursor < totalLength;
|
|
200
|
-
const percent = totalLength
|
|
201
|
-
|
|
205
|
+
const percent = totalLength
|
|
206
|
+
? Math.round((nextCursor / totalLength) * 100)
|
|
207
|
+
: 100;
|
|
208
|
+
lines.push(`\nBody Chunk (text): ${percent}%${more ? " (more)" : " (done)"}:`);
|
|
202
209
|
lines.push(chunk);
|
|
203
210
|
if (more)
|
|
204
|
-
lines.push(formatTruncationLine(nextCursor,
|
|
211
|
+
lines.push(formatTruncationLine(nextCursor, totalLength, { formatter: formatBytes }));
|
|
205
212
|
state.cursor = nextCursor;
|
|
206
213
|
state.headersShown = true;
|
|
207
|
-
return lines.join(
|
|
214
|
+
return lines.join("\n");
|
|
208
215
|
};
|
|
209
216
|
const networkDetail = defineTabTool({
|
|
210
|
-
capability:
|
|
217
|
+
capability: "core",
|
|
211
218
|
schema: {
|
|
212
|
-
name:
|
|
213
|
-
title:
|
|
214
|
-
description:
|
|
219
|
+
name: "browser_network_detail",
|
|
220
|
+
title: "Get network request detail",
|
|
221
|
+
description: "Show detailed info for a specific network request by event id. Call repeatedly to page through the body.",
|
|
215
222
|
inputSchema: networkDetailSchema,
|
|
216
|
-
type:
|
|
223
|
+
type: "readOnly",
|
|
217
224
|
},
|
|
218
225
|
handle: async (tab, params, r) => {
|
|
219
226
|
const entry = getNetworkEventEntry(tab.context, params.id);
|