chrome-devtools-mcp 0.21.0 → 0.23.0
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 +87 -21
- package/build/src/HeapSnapshotManager.js +94 -0
- package/build/src/McpContext.js +26 -181
- package/build/src/McpPage.js +214 -0
- package/build/src/McpResponse.js +151 -13
- package/build/src/PageCollector.js +10 -24
- package/build/src/TextSnapshot.js +230 -0
- package/build/src/WaitForHelper.js +31 -0
- package/build/src/bin/check-latest-version.js +25 -0
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +34 -10
- package/build/src/bin/chrome-devtools-mcp-main.js +2 -0
- package/build/src/bin/chrome-devtools.js +25 -14
- package/build/src/bin/cliDefinitions.js +14 -8
- package/build/src/daemon/client.js +11 -11
- package/build/src/daemon/daemon.js +6 -9
- package/build/src/daemon/utils.js +19 -14
- package/build/src/formatters/HeapSnapshotFormatter.js +38 -0
- package/build/src/formatters/NetworkFormatter.js +24 -7
- package/build/src/index.js +12 -1
- package/build/src/telemetry/ClearcutLogger.js +34 -12
- package/build/src/telemetry/flagUtils.js +46 -4
- package/build/src/telemetry/toolMetricsUtils.js +88 -0
- package/build/src/telemetry/watchdog/ClearcutSender.js +4 -3
- package/build/src/third_party/THIRD_PARTY_NOTICES +59 -32
- package/build/src/third_party/bundled-packages.json +6 -4
- package/build/src/third_party/devtools-formatter-worker.js +61 -64
- package/build/src/third_party/devtools-heap-snapshot-worker.js +9690 -0
- package/build/src/third_party/index.js +62661 -60590
- package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +3501 -2658
- package/build/src/tools/categories.js +3 -0
- package/build/src/tools/console.js +42 -39
- package/build/src/tools/emulation.js +1 -1
- package/build/src/tools/extensions.js +5 -11
- package/build/src/tools/inPage.js +3 -13
- package/build/src/tools/input.js +15 -16
- package/build/src/tools/lighthouse.js +2 -2
- package/build/src/tools/memory.js +48 -3
- package/build/src/tools/network.js +4 -4
- package/build/src/tools/pages.js +212 -146
- package/build/src/tools/performance.js +1 -1
- package/build/src/tools/screencast.js +20 -8
- package/build/src/tools/screenshot.js +3 -3
- package/build/src/tools/script.js +22 -16
- package/build/src/tools/tools.js +2 -0
- package/build/src/tools/webmcp.js +63 -0
- package/build/src/utils/check-for-updates.js +73 -0
- package/build/src/utils/files.js +4 -0
- package/build/src/utils/id.js +15 -0
- package/build/src/version.js +1 -1
- package/package.json +13 -8
- package/build/src/utils/ExtensionRegistry.js +0 -35
package/build/src/McpPage.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
|
+
import { TextSnapshot } from './TextSnapshot.js';
|
|
6
8
|
import { takeSnapshot } from './tools/snapshot.js';
|
|
9
|
+
import { getNetworkMultiplierFromString, WaitForHelper, } from './WaitForHelper.js';
|
|
7
10
|
/**
|
|
8
11
|
* Per-page state wrapper. Consolidates dialog, snapshot, emulation,
|
|
9
12
|
* and metadata that were previously scattered across Maps in McpContext.
|
|
@@ -18,6 +21,7 @@ export class McpPage {
|
|
|
18
21
|
// Snapshot
|
|
19
22
|
textSnapshot = null;
|
|
20
23
|
uniqueBackendNodeIdToMcpId = new Map();
|
|
24
|
+
extraHandles = [];
|
|
21
25
|
// Emulation
|
|
22
26
|
emulationSettings = {};
|
|
23
27
|
// Metadata
|
|
@@ -26,6 +30,7 @@ export class McpPage {
|
|
|
26
30
|
// Dialog
|
|
27
31
|
#dialog;
|
|
28
32
|
#dialogHandler;
|
|
33
|
+
inPageTools;
|
|
29
34
|
constructor(page, id) {
|
|
30
35
|
this.pptrPage = page;
|
|
31
36
|
this.id = id;
|
|
@@ -43,6 +48,12 @@ export class McpPage {
|
|
|
43
48
|
clearDialog() {
|
|
44
49
|
this.#dialog = undefined;
|
|
45
50
|
}
|
|
51
|
+
getInPageTools() {
|
|
52
|
+
return this.inPageTools;
|
|
53
|
+
}
|
|
54
|
+
getWebMcpTools() {
|
|
55
|
+
return this.pptrPage.webmcp.tools();
|
|
56
|
+
}
|
|
46
57
|
get networkConditions() {
|
|
47
58
|
return this.emulationSettings.networkConditions ?? null;
|
|
48
59
|
}
|
|
@@ -61,9 +72,162 @@ export class McpPage {
|
|
|
61
72
|
get colorScheme() {
|
|
62
73
|
return this.emulationSettings.colorScheme ?? null;
|
|
63
74
|
}
|
|
75
|
+
// Public for testability: tests spy on this method to verify throttle multipliers.
|
|
76
|
+
createWaitForHelper(cpuMultiplier, networkMultiplier) {
|
|
77
|
+
return new WaitForHelper(this.pptrPage, cpuMultiplier, networkMultiplier);
|
|
78
|
+
}
|
|
79
|
+
waitForEventsAfterAction(action, options) {
|
|
80
|
+
const helper = this.createWaitForHelper(this.cpuThrottlingRate, getNetworkMultiplierFromString(this.networkConditions));
|
|
81
|
+
return helper.waitForEventsAfterAction(action, options);
|
|
82
|
+
}
|
|
64
83
|
dispose() {
|
|
65
84
|
this.pptrPage.off('dialog', this.#dialogHandler);
|
|
66
85
|
}
|
|
86
|
+
async executeInPageTool(toolName, params, response) {
|
|
87
|
+
// Creates array of ElementHandles from the UIDs in the params.
|
|
88
|
+
// We do not replace the uids with the ElementsHandles yet, because
|
|
89
|
+
// the `evaluate` function only turns them into DOM elements if they
|
|
90
|
+
// are passed as non-nested arguments.
|
|
91
|
+
const handles = [];
|
|
92
|
+
for (const value of Object.values(params)) {
|
|
93
|
+
if (value instanceof Object &&
|
|
94
|
+
'uid' in value &&
|
|
95
|
+
typeof value.uid === 'string' &&
|
|
96
|
+
Object.keys(value).length === 1) {
|
|
97
|
+
handles.push(await this.getElementByUid(value.uid));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const result = await this.pptrPage.evaluate(async (name, args, ...elements) => {
|
|
101
|
+
// Replace the UIDs with DOM elements.
|
|
102
|
+
for (const [key, value] of Object.entries(args)) {
|
|
103
|
+
if (value instanceof Object &&
|
|
104
|
+
'uid' in value &&
|
|
105
|
+
typeof value.uid === 'string' &&
|
|
106
|
+
Object.keys(value).length === 1) {
|
|
107
|
+
args[key] = elements.shift();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!window.__dtmcp?.executeTool) {
|
|
111
|
+
throw new Error('No tools found on the page');
|
|
112
|
+
}
|
|
113
|
+
const toolResult = await window.__dtmcp.executeTool(name, args);
|
|
114
|
+
const stashDOMElement = (el) => {
|
|
115
|
+
if (!window.__dtmcp) {
|
|
116
|
+
window.__dtmcp = {};
|
|
117
|
+
}
|
|
118
|
+
if (window.__dtmcp.stashedElements === undefined) {
|
|
119
|
+
window.__dtmcp.stashedElements = [];
|
|
120
|
+
}
|
|
121
|
+
window.__dtmcp.stashedElements.push(el);
|
|
122
|
+
return {
|
|
123
|
+
stashedId: `stashed-${window.__dtmcp.stashedElements.length - 1}`,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
const ancestors = [];
|
|
127
|
+
// Recursively walks the tool result:
|
|
128
|
+
// - Replaces DOM elements with an ID and stashes the DOM element on the window object
|
|
129
|
+
// - Replaces non-plain objects with a string representation of the object
|
|
130
|
+
// - Replaces circular references with the string '<Circular reference>'
|
|
131
|
+
// - Replaces functions with the string '<Function object>'
|
|
132
|
+
const processToolResult = (data, parentEl) => {
|
|
133
|
+
// 1. Handle DOM Elements
|
|
134
|
+
if (data instanceof Element) {
|
|
135
|
+
return stashDOMElement(data);
|
|
136
|
+
}
|
|
137
|
+
// 2. Handle Arrays
|
|
138
|
+
if (Array.isArray(data)) {
|
|
139
|
+
return data.map((item) => processToolResult(item, parentEl));
|
|
140
|
+
}
|
|
141
|
+
// 3. Handle Objects
|
|
142
|
+
if (data !== null && typeof data === 'object') {
|
|
143
|
+
while (ancestors.length > 0 && ancestors.at(-1) !== parentEl) {
|
|
144
|
+
ancestors.pop();
|
|
145
|
+
}
|
|
146
|
+
if (ancestors.includes(data)) {
|
|
147
|
+
return '<Circular reference>';
|
|
148
|
+
}
|
|
149
|
+
ancestors.push(data);
|
|
150
|
+
// If not a plain object, return a string representation of the object
|
|
151
|
+
if (Object.getPrototypeOf(data) !== Object.prototype) {
|
|
152
|
+
return `<${data.constructor.name} instance>`;
|
|
153
|
+
}
|
|
154
|
+
const processedObj = {};
|
|
155
|
+
for (const [key, value] of Object.entries(data)) {
|
|
156
|
+
processedObj[key] = processToolResult(value, data);
|
|
157
|
+
}
|
|
158
|
+
return processedObj;
|
|
159
|
+
}
|
|
160
|
+
// 4. Handle Functions
|
|
161
|
+
if (typeof data === 'function') {
|
|
162
|
+
return '<Function object>';
|
|
163
|
+
}
|
|
164
|
+
// 5. Return primitives (strings, numbers, booleans) as-is
|
|
165
|
+
return data;
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
result: processToolResult(toolResult),
|
|
169
|
+
stashed: window.__dtmcp?.stashedElements?.length ?? 0,
|
|
170
|
+
};
|
|
171
|
+
}, toolName, params, ...handles);
|
|
172
|
+
const elementHandles = [];
|
|
173
|
+
for (let i = 0; i < (result.stashed ?? 0); i++) {
|
|
174
|
+
const elementHandle = await this.pptrPage.evaluateHandle(index => {
|
|
175
|
+
const el = window.__dtmcp?.stashedElements?.[index];
|
|
176
|
+
if (!el) {
|
|
177
|
+
throw new Error(`Stashed element at index ${index} not found`);
|
|
178
|
+
}
|
|
179
|
+
return el;
|
|
180
|
+
}, i);
|
|
181
|
+
elementHandles.push(elementHandle);
|
|
182
|
+
}
|
|
183
|
+
if (elementHandles.length) {
|
|
184
|
+
const oldHandles = [...this.extraHandles];
|
|
185
|
+
this.textSnapshot = await TextSnapshot.create(this, {
|
|
186
|
+
extraHandles: elementHandles,
|
|
187
|
+
});
|
|
188
|
+
response.includeSnapshot();
|
|
189
|
+
for (const handle of oldHandles) {
|
|
190
|
+
await handle
|
|
191
|
+
.dispose()
|
|
192
|
+
.catch(e => logger('Failed to dispose old handle', e));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const cdpElementIds = await Promise.all(elementHandles.map(async (elementHandle, index) => {
|
|
196
|
+
const backendNodeId = await elementHandle.backendNodeId();
|
|
197
|
+
if (!backendNodeId) {
|
|
198
|
+
logger(`No backendNodeId for stashed DOM element with index ${index}`);
|
|
199
|
+
return `stashed-${index}`;
|
|
200
|
+
}
|
|
201
|
+
const cdpElementId = this.resolveCdpElementId(backendNodeId);
|
|
202
|
+
if (!cdpElementId) {
|
|
203
|
+
logger(`Could not get cdpElementId for backend node ${backendNodeId}`);
|
|
204
|
+
return `stashed-${index}`;
|
|
205
|
+
}
|
|
206
|
+
return cdpElementId;
|
|
207
|
+
}));
|
|
208
|
+
const recursivelyReplaceStashedElements = (node) => {
|
|
209
|
+
if (Array.isArray(node)) {
|
|
210
|
+
return node.map(x => recursivelyReplaceStashedElements(x));
|
|
211
|
+
}
|
|
212
|
+
if (node !== null && typeof node === 'object') {
|
|
213
|
+
if ('stashedId' in node &&
|
|
214
|
+
typeof node.stashedId === 'string' &&
|
|
215
|
+
node.stashedId.startsWith('stashed-') &&
|
|
216
|
+
Object.keys(node).length === 1) {
|
|
217
|
+
const index = parseInt(node.stashedId.split('-')[1]);
|
|
218
|
+
return { uid: cdpElementIds[index] };
|
|
219
|
+
}
|
|
220
|
+
const resultObj = {};
|
|
221
|
+
for (const [key, value] of Object.entries(node)) {
|
|
222
|
+
resultObj[key] = recursivelyReplaceStashedElements(value);
|
|
223
|
+
}
|
|
224
|
+
return resultObj;
|
|
225
|
+
}
|
|
226
|
+
return node;
|
|
227
|
+
};
|
|
228
|
+
const resultWithUids = recursivelyReplaceStashedElements(result.result);
|
|
229
|
+
response.appendResponseLine(JSON.stringify(resultWithUids, null, 2));
|
|
230
|
+
}
|
|
67
231
|
async getElementByUid(uid) {
|
|
68
232
|
if (!this.textSnapshot) {
|
|
69
233
|
throw new Error(`No snapshot found for page ${this.id ?? '?'}. Use ${takeSnapshot.name} to capture one.`);
|
|
@@ -92,4 +256,54 @@ export class McpPage {
|
|
|
92
256
|
getAXNodeByUid(uid) {
|
|
93
257
|
return this.textSnapshot?.idToNode.get(uid);
|
|
94
258
|
}
|
|
259
|
+
resolveCdpElementId(cdpBackendNodeId) {
|
|
260
|
+
if (!cdpBackendNodeId) {
|
|
261
|
+
logger('no cdpBackendNodeId');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const snapshot = this.textSnapshot;
|
|
265
|
+
if (!snapshot) {
|
|
266
|
+
logger('no text snapshot');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// TODO: index by backendNodeId instead.
|
|
270
|
+
const queue = [snapshot.root];
|
|
271
|
+
while (queue.length) {
|
|
272
|
+
const current = queue.pop();
|
|
273
|
+
if (current.backendNodeId === cdpBackendNodeId) {
|
|
274
|
+
return current.id;
|
|
275
|
+
}
|
|
276
|
+
for (const child of current.children) {
|
|
277
|
+
queue.push(child);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
async getDevToolsData() {
|
|
283
|
+
try {
|
|
284
|
+
logger('Getting DevTools UI data');
|
|
285
|
+
const devtoolsPage = this.devToolsPage;
|
|
286
|
+
if (!devtoolsPage) {
|
|
287
|
+
logger('No DevTools page detected');
|
|
288
|
+
return {};
|
|
289
|
+
}
|
|
290
|
+
const { cdpRequestId, cdpBackendNodeId } = await devtoolsPage.evaluate(async () => {
|
|
291
|
+
// @ts-expect-error no types
|
|
292
|
+
const UI = await import('/bundled/ui/legacy/legacy.js');
|
|
293
|
+
// @ts-expect-error no types
|
|
294
|
+
const SDK = await import('/bundled/core/sdk/sdk.js');
|
|
295
|
+
const request = UI.Context.Context.instance().flavor(SDK.NetworkRequest.NetworkRequest);
|
|
296
|
+
const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
|
|
297
|
+
return {
|
|
298
|
+
cdpRequestId: request?.requestId(),
|
|
299
|
+
cdpBackendNodeId: node?.backendNodeId(),
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
return { cdpBackendNodeId, cdpRequestId };
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
logger('error getting devtools data', err);
|
|
306
|
+
}
|
|
307
|
+
return {};
|
|
308
|
+
}
|
|
95
309
|
}
|
package/build/src/McpResponse.js
CHANGED
|
@@ -4,14 +4,62 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import { ConsoleFormatter } from './formatters/ConsoleFormatter.js';
|
|
7
|
+
import { HeapSnapshotFormatter } from './formatters/HeapSnapshotFormatter.js';
|
|
7
8
|
import { IssueFormatter } from './formatters/IssueFormatter.js';
|
|
8
9
|
import { NetworkFormatter } from './formatters/NetworkFormatter.js';
|
|
9
10
|
import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
|
|
10
11
|
import { UncaughtError } from './PageCollector.js';
|
|
12
|
+
import { TextSnapshot } from './TextSnapshot.js';
|
|
11
13
|
import { DevTools } from './third_party/index.js';
|
|
12
14
|
import { handleDialog } from './tools/pages.js';
|
|
13
15
|
import { getInsightOutput, getTraceSummary } from './trace-processing/parse.js';
|
|
14
16
|
import { paginate } from './utils/pagination.js';
|
|
17
|
+
export function replaceHtmlElementsWithUids(schema) {
|
|
18
|
+
if (typeof schema === 'boolean') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let isHtmlElement = false;
|
|
22
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
23
|
+
if (key === 'x-mcp-type' && value === 'HTMLElement') {
|
|
24
|
+
isHtmlElement = true;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (isHtmlElement) {
|
|
29
|
+
schema.properties = { uid: { type: 'string' } };
|
|
30
|
+
schema.required = ['uid'];
|
|
31
|
+
}
|
|
32
|
+
if (schema.properties) {
|
|
33
|
+
for (const key of Object.keys(schema.properties)) {
|
|
34
|
+
replaceHtmlElementsWithUids(schema.properties[key]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (schema.items) {
|
|
38
|
+
if (Array.isArray(schema.items)) {
|
|
39
|
+
for (const item of schema.items) {
|
|
40
|
+
replaceHtmlElementsWithUids(item);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
replaceHtmlElementsWithUids(schema.items);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (schema.anyOf) {
|
|
48
|
+
for (const s of schema.anyOf) {
|
|
49
|
+
replaceHtmlElementsWithUids(s);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (schema.allOf) {
|
|
53
|
+
for (const s of schema.allOf) {
|
|
54
|
+
replaceHtmlElementsWithUids(s);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (schema.oneOf) {
|
|
58
|
+
for (const s of schema.oneOf) {
|
|
59
|
+
replaceHtmlElementsWithUids(s);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
15
63
|
async function getToolGroup(page) {
|
|
16
64
|
// Check if there is a `devtoolstooldiscovery` event listener
|
|
17
65
|
const windowHandle = await page.pptrPage.evaluateHandle(() => window);
|
|
@@ -54,6 +102,9 @@ async function getToolGroup(page) {
|
|
|
54
102
|
}, 0);
|
|
55
103
|
});
|
|
56
104
|
});
|
|
105
|
+
for (const tool of toolGroup?.tools ?? []) {
|
|
106
|
+
replaceHtmlElementsWithUids(tool.inputSchema);
|
|
107
|
+
}
|
|
57
108
|
return toolGroup;
|
|
58
109
|
}
|
|
59
110
|
export class McpResponse {
|
|
@@ -69,20 +120,26 @@ export class McpResponse {
|
|
|
69
120
|
#attachedLighthouseResult;
|
|
70
121
|
#textResponseLines = [];
|
|
71
122
|
#images = [];
|
|
123
|
+
#heapSnapshotOptions;
|
|
72
124
|
#networkRequestsOptions;
|
|
73
125
|
#consoleDataOptions;
|
|
74
126
|
#listExtensions;
|
|
75
127
|
#listInPageTools;
|
|
128
|
+
#listWebMcpTools;
|
|
76
129
|
#devToolsData;
|
|
77
130
|
#tabId;
|
|
78
131
|
#args;
|
|
79
132
|
#page;
|
|
133
|
+
#redactNetworkHeaders = true;
|
|
80
134
|
constructor(args) {
|
|
81
135
|
this.#args = args;
|
|
82
136
|
}
|
|
83
137
|
setPage(page) {
|
|
84
138
|
this.#page = page;
|
|
85
139
|
}
|
|
140
|
+
setRedactNetworkHeaders(value) {
|
|
141
|
+
this.#redactNetworkHeaders = value;
|
|
142
|
+
}
|
|
86
143
|
attachDevToolsData(data) {
|
|
87
144
|
this.#devToolsData = data;
|
|
88
145
|
}
|
|
@@ -109,6 +166,9 @@ export class McpResponse {
|
|
|
109
166
|
this.#listInPageTools = true;
|
|
110
167
|
}
|
|
111
168
|
}
|
|
169
|
+
setListWebMcpTools() {
|
|
170
|
+
this.#listWebMcpTools = true;
|
|
171
|
+
}
|
|
112
172
|
setIncludeNetworkRequests(value, options) {
|
|
113
173
|
if (!value) {
|
|
114
174
|
this.#networkRequestsOptions = undefined;
|
|
@@ -197,6 +257,22 @@ export class McpResponse {
|
|
|
197
257
|
appendResponseLine(value) {
|
|
198
258
|
this.#textResponseLines.push(value);
|
|
199
259
|
}
|
|
260
|
+
setHeapSnapshotAggregates(aggregates, options) {
|
|
261
|
+
this.#heapSnapshotOptions = {
|
|
262
|
+
...this.#heapSnapshotOptions,
|
|
263
|
+
include: true,
|
|
264
|
+
aggregates,
|
|
265
|
+
pagination: options,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
setHeapSnapshotStats(stats, staticData) {
|
|
269
|
+
this.#heapSnapshotOptions = {
|
|
270
|
+
...this.#heapSnapshotOptions,
|
|
271
|
+
include: true,
|
|
272
|
+
stats,
|
|
273
|
+
staticData,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
200
276
|
attachImage(value) {
|
|
201
277
|
this.#images.push(value);
|
|
202
278
|
}
|
|
@@ -209,6 +285,9 @@ export class McpResponse {
|
|
|
209
285
|
get snapshotParams() {
|
|
210
286
|
return this.#snapshotParams;
|
|
211
287
|
}
|
|
288
|
+
get listWebMcpTools() {
|
|
289
|
+
return this.#listWebMcpTools;
|
|
290
|
+
}
|
|
212
291
|
async handle(toolName, context) {
|
|
213
292
|
if (this.#includePages) {
|
|
214
293
|
await context.createPagesSnapshot();
|
|
@@ -221,13 +300,16 @@ export class McpResponse {
|
|
|
221
300
|
if (!this.#page) {
|
|
222
301
|
throw new Error('Response must have a page');
|
|
223
302
|
}
|
|
224
|
-
await
|
|
303
|
+
this.#page.textSnapshot = await TextSnapshot.create(this.#page, {
|
|
304
|
+
verbose: this.#snapshotParams.verbose,
|
|
305
|
+
devtoolsData: this.#devToolsData,
|
|
306
|
+
});
|
|
225
307
|
const textSnapshot = this.#page.textSnapshot;
|
|
226
308
|
if (textSnapshot) {
|
|
227
309
|
const formatter = new SnapshotFormatter(textSnapshot);
|
|
228
310
|
if (this.#snapshotParams.filePath) {
|
|
229
|
-
await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath);
|
|
230
|
-
snapshot =
|
|
311
|
+
const result = await context.saveFile(new TextEncoder().encode(formatter.toString()), this.#snapshotParams.filePath, '.txt');
|
|
312
|
+
snapshot = result.filename;
|
|
231
313
|
}
|
|
232
314
|
else {
|
|
233
315
|
snapshot = formatter;
|
|
@@ -246,7 +328,8 @@ export class McpResponse {
|
|
|
246
328
|
fetchData: true,
|
|
247
329
|
requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath,
|
|
248
330
|
responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath,
|
|
249
|
-
saveFile: (data, filename) => context.saveFile(data, filename),
|
|
331
|
+
saveFile: (data, filename, extension) => context.saveFile(data, filename, extension),
|
|
332
|
+
redactNetworkHeaders: this.#redactNetworkHeaders,
|
|
250
333
|
});
|
|
251
334
|
detailedNetworkRequest = formatter;
|
|
252
335
|
}
|
|
@@ -270,7 +353,7 @@ export class McpResponse {
|
|
|
270
353
|
const formatter = new IssueFormatter(message, {
|
|
271
354
|
id: consoleMessageStableId,
|
|
272
355
|
requestIdResolver: context.resolveCdpRequestId.bind(context, this.#page),
|
|
273
|
-
elementIdResolver:
|
|
356
|
+
elementIdResolver: this.#page.resolveCdpElementId.bind(this.#page),
|
|
274
357
|
});
|
|
275
358
|
if (!formatter.isValid()) {
|
|
276
359
|
throw new Error("Can't provide details for the msgid " + consoleMessageStableId);
|
|
@@ -280,12 +363,18 @@ export class McpResponse {
|
|
|
280
363
|
}
|
|
281
364
|
let extensions;
|
|
282
365
|
if (this.#listExtensions) {
|
|
283
|
-
extensions = context.listExtensions();
|
|
366
|
+
extensions = await context.listExtensions();
|
|
284
367
|
}
|
|
285
368
|
let inPageTools;
|
|
286
369
|
if (this.#listInPageTools) {
|
|
287
|
-
|
|
288
|
-
|
|
370
|
+
const page = this.#page ?? context.getSelectedMcpPage();
|
|
371
|
+
inPageTools = await getToolGroup(page);
|
|
372
|
+
page.inPageTools = inPageTools;
|
|
373
|
+
}
|
|
374
|
+
let webmcpTools;
|
|
375
|
+
if (this.#listWebMcpTools && this.#args.experimentalWebmcp) {
|
|
376
|
+
const page = this.#page ?? context.getSelectedMcpPage();
|
|
377
|
+
webmcpTools = page.getWebMcpTools();
|
|
289
378
|
}
|
|
290
379
|
let consoleMessages;
|
|
291
380
|
if (this.#consoleDataOptions?.include) {
|
|
@@ -349,7 +438,8 @@ export class McpResponse {
|
|
|
349
438
|
selectedInDevToolsUI: context.getNetworkRequestStableId(request) ===
|
|
350
439
|
this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
|
|
351
440
|
fetchData: false,
|
|
352
|
-
saveFile: (data, filename) => context.saveFile(data, filename),
|
|
441
|
+
saveFile: (data, filename, extension) => context.saveFile(data, filename, extension),
|
|
442
|
+
redactNetworkHeaders: this.#redactNetworkHeaders,
|
|
353
443
|
})));
|
|
354
444
|
}
|
|
355
445
|
}
|
|
@@ -364,6 +454,7 @@ export class McpResponse {
|
|
|
364
454
|
extensions,
|
|
365
455
|
lighthouseResult: this.#attachedLighthouseResult,
|
|
366
456
|
inPageTools,
|
|
457
|
+
webmcpTools,
|
|
367
458
|
});
|
|
368
459
|
}
|
|
369
460
|
format(toolName, context, data) {
|
|
@@ -529,6 +620,32 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
529
620
|
structuredContent.snapshot = data.snapshot.toJSON();
|
|
530
621
|
}
|
|
531
622
|
}
|
|
623
|
+
if (this.#heapSnapshotOptions?.include) {
|
|
624
|
+
response.push('## Heap Snapshot Data');
|
|
625
|
+
const stats = this.#heapSnapshotOptions.stats;
|
|
626
|
+
const staticData = this.#heapSnapshotOptions.staticData;
|
|
627
|
+
if (stats) {
|
|
628
|
+
response.push(`Statistics: ${JSON.stringify(stats, null, 2)}`);
|
|
629
|
+
structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
|
|
630
|
+
structuredContent.heapSnapshot.stats = stats;
|
|
631
|
+
}
|
|
632
|
+
if (staticData) {
|
|
633
|
+
response.push(`Static Data: ${JSON.stringify(staticData, null, 2)}`);
|
|
634
|
+
structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
|
|
635
|
+
structuredContent.heapSnapshot.staticData = staticData;
|
|
636
|
+
}
|
|
637
|
+
const aggregates = this.#heapSnapshotOptions.aggregates;
|
|
638
|
+
if (aggregates) {
|
|
639
|
+
const sortedEntries = HeapSnapshotFormatter.sort(aggregates);
|
|
640
|
+
const paginationData = this.#dataWithPagination(sortedEntries, this.#heapSnapshotOptions.pagination);
|
|
641
|
+
structuredContent.pagination = paginationData.pagination;
|
|
642
|
+
response.push(...paginationData.info);
|
|
643
|
+
const paginatedRecord = Object.fromEntries(paginationData.items);
|
|
644
|
+
const formatter = new HeapSnapshotFormatter(paginatedRecord);
|
|
645
|
+
response.push(formatter.toString());
|
|
646
|
+
structuredContent.heapSnapshotData = formatter.toJSON();
|
|
647
|
+
}
|
|
648
|
+
}
|
|
532
649
|
if (data.detailedNetworkRequest) {
|
|
533
650
|
response.push(data.detailedNetworkRequest.toStringDetailed());
|
|
534
651
|
structuredContent.networkRequest =
|
|
@@ -540,15 +657,16 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
540
657
|
data.detailedConsoleMessage.toJSONDetailed();
|
|
541
658
|
}
|
|
542
659
|
if (data.extensions) {
|
|
543
|
-
|
|
660
|
+
const extensionArray = Array.from(data.extensions.values());
|
|
661
|
+
structuredContent.extensions = extensionArray;
|
|
544
662
|
response.push('## Extensions');
|
|
545
|
-
if (
|
|
663
|
+
if (extensionArray.length === 0) {
|
|
546
664
|
response.push('No extensions installed.');
|
|
547
665
|
}
|
|
548
666
|
else {
|
|
549
|
-
const extensionsMessage =
|
|
667
|
+
const extensionsMessage = extensionArray
|
|
550
668
|
.map(extension => {
|
|
551
|
-
return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.
|
|
669
|
+
return `id=${extension.id} "${extension.name}" v${extension.version} ${extension.enabled ? 'Enabled' : 'Disabled'}`;
|
|
552
670
|
})
|
|
553
671
|
.join('\n');
|
|
554
672
|
response.push(extensionsMessage);
|
|
@@ -572,6 +690,26 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
572
690
|
response.push(toolDefinitionsMessage);
|
|
573
691
|
}
|
|
574
692
|
}
|
|
693
|
+
if (this.#listWebMcpTools && data.webmcpTools) {
|
|
694
|
+
structuredContent.webmcpTools = data.webmcpTools.map(({ name, description, inputSchema, annotations }) => ({
|
|
695
|
+
name,
|
|
696
|
+
description,
|
|
697
|
+
inputSchema,
|
|
698
|
+
annotations,
|
|
699
|
+
}));
|
|
700
|
+
response.push('## WebMCP tools');
|
|
701
|
+
if (data.webmcpTools.length === 0) {
|
|
702
|
+
response.push('No WebMCP tools available.');
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
const webmcpToolsMessage = data.webmcpTools
|
|
706
|
+
.map(tool => {
|
|
707
|
+
return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(tool.inputSchema)}, annotations=${JSON.stringify(tool.annotations)}`;
|
|
708
|
+
})
|
|
709
|
+
.join('\n');
|
|
710
|
+
response.push(webmcpToolsMessage);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
575
713
|
if (this.#networkRequestsOptions?.include && data.networkRequests) {
|
|
576
714
|
const requests = data.networkRequests;
|
|
577
715
|
response.push('## Network requests');
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { FakeIssuesManager } from './DevtoolsUtils.js';
|
|
7
7
|
import { logger } from './logger.js';
|
|
8
8
|
import { DevTools } from './third_party/index.js';
|
|
9
|
+
import { createIdGenerator, stableIdSymbol, } from './utils/id.js';
|
|
9
10
|
export class UncaughtError {
|
|
10
11
|
details;
|
|
11
12
|
targetId;
|
|
@@ -14,16 +15,6 @@ export class UncaughtError {
|
|
|
14
15
|
this.targetId = targetId;
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
|
-
function createIdGenerator() {
|
|
18
|
-
let i = 1;
|
|
19
|
-
return () => {
|
|
20
|
-
if (i === Number.MAX_SAFE_INTEGER) {
|
|
21
|
-
i = 0;
|
|
22
|
-
}
|
|
23
|
-
return i++;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
export const stableIdSymbol = Symbol('stableIdSymbol');
|
|
27
18
|
export class PageCollector {
|
|
28
19
|
#browser;
|
|
29
20
|
#listenersInitializer;
|
|
@@ -206,34 +197,25 @@ class PageEventSubscriber {
|
|
|
206
197
|
async subscribe() {
|
|
207
198
|
this.#resetIssueAggregator();
|
|
208
199
|
this.#page.on('framenavigated', this.#onFrameNavigated);
|
|
209
|
-
this.#
|
|
200
|
+
this.#page.on('issue', this.#onIssueAdded);
|
|
210
201
|
this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown);
|
|
211
|
-
try {
|
|
212
|
-
await this.#session.send('Audits.enable');
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
logger('Error subscribing to issues', error);
|
|
216
|
-
}
|
|
217
202
|
}
|
|
218
203
|
unsubscribe() {
|
|
219
204
|
this.#seenKeys.clear();
|
|
220
205
|
this.#seenIssues.clear();
|
|
221
206
|
this.#page.off('framenavigated', this.#onFrameNavigated);
|
|
222
|
-
this.#
|
|
207
|
+
this.#page.off('issue', this.#onIssueAdded);
|
|
223
208
|
this.#session.off('Runtime.exceptionThrown', this.#onExceptionThrown);
|
|
224
209
|
if (this.#issueAggregator) {
|
|
225
210
|
this.#issueAggregator.removeEventListener("AggregatedIssueUpdated" /* DevTools.IssueAggregatorEvents.AGGREGATED_ISSUE_UPDATED */, this.#onAggregatedIssue);
|
|
226
211
|
}
|
|
227
|
-
void this.#session.send('Audits.disable').catch(() => {
|
|
228
|
-
// might fail.
|
|
229
|
-
});
|
|
230
212
|
}
|
|
231
213
|
#onAggregatedIssue = (event) => {
|
|
232
214
|
if (this.#seenIssues.has(event.data)) {
|
|
233
215
|
return;
|
|
234
216
|
}
|
|
235
217
|
this.#seenIssues.add(event.data);
|
|
236
|
-
this.#page.emit('
|
|
218
|
+
this.#page.emit('devtoolsAggregatedIssue', event.data);
|
|
237
219
|
};
|
|
238
220
|
#onExceptionThrown = (event) => {
|
|
239
221
|
this.#page.emit('uncaughtError', new UncaughtError(event.exceptionDetails, this.#targetId));
|
|
@@ -248,9 +230,13 @@ class PageEventSubscriber {
|
|
|
248
230
|
this.#seenIssues.clear();
|
|
249
231
|
this.#resetIssueAggregator();
|
|
250
232
|
};
|
|
251
|
-
#onIssueAdded = (
|
|
233
|
+
#onIssueAdded = (inspectorIssue) => {
|
|
252
234
|
try {
|
|
253
|
-
|
|
235
|
+
// DevTools currently defines this protocol issue code but has no
|
|
236
|
+
// IssuesManager handler for it, so calling into the mapper only warns.
|
|
237
|
+
if (String(inspectorIssue.code) === 'PerformanceIssue') {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
254
240
|
const issue = DevTools.createIssuesFromProtocolIssue(null,
|
|
255
241
|
// @ts-expect-error Protocol types diverge.
|
|
256
242
|
inspectorIssue)[0];
|