chrome-devtools-mcp 0.22.0 → 0.24.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 +4 -0
- package/build/src/DevToolsConnectionAdapter.js +1 -0
- package/build/src/DevtoolsUtils.js +1 -0
- package/build/src/HeapSnapshotManager.js +16 -0
- package/build/src/McpContext.js +54 -126
- package/build/src/McpPage.js +204 -0
- package/build/src/McpResponse.js +44 -6
- package/build/src/Mutex.js +1 -0
- package/build/src/PageCollector.js +1 -0
- package/build/src/SlimMcpResponse.js +1 -0
- package/build/src/TextSnapshot.js +236 -0
- package/build/src/WaitForHelper.js +6 -0
- package/build/src/bin/check-latest-version.js +1 -0
- package/build/src/bin/chrome-devtools-cli-options.js +206 -46
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +13 -1
- package/build/src/bin/chrome-devtools-mcp-main.js +1 -0
- package/build/src/bin/chrome-devtools-mcp.js +1 -0
- package/build/src/bin/chrome-devtools.js +27 -27
- package/build/src/browser.js +1 -0
- package/build/src/daemon/client.js +14 -12
- package/build/src/daemon/daemon.js +7 -5
- package/build/src/daemon/types.js +1 -0
- package/build/src/daemon/utils.js +20 -14
- package/build/src/formatters/ConsoleFormatter.js +48 -1
- package/build/src/formatters/HeapSnapshotFormatter.js +18 -2
- package/build/src/formatters/IssueFormatter.js +1 -0
- package/build/src/formatters/NetworkFormatter.js +1 -0
- package/build/src/formatters/SnapshotFormatter.js +2 -1
- package/build/src/index.js +114 -51
- package/build/src/issue-descriptions.js +1 -0
- package/build/src/logger.js +1 -0
- package/build/src/polyfill.js +1 -0
- package/build/src/telemetry/ClearcutLogger.js +13 -1
- package/build/src/telemetry/WatchdogClient.js +1 -0
- package/build/src/telemetry/flagUtils.js +1 -0
- package/build/src/telemetry/metricUtils.js +1 -0
- package/build/src/telemetry/persistence.js +1 -0
- package/build/src/telemetry/toolMetricsUtils.js +2 -1
- package/build/src/telemetry/types.js +1 -0
- package/build/src/telemetry/watchdog/ClearcutSender.js +1 -0
- package/build/src/telemetry/watchdog/main.js +1 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +32 -5
- package/build/src/third_party/bundled-packages.json +3 -2
- package/build/src/third_party/devtools-formatter-worker.js +2451 -2933
- package/build/src/third_party/devtools-heap-snapshot-worker.js +32 -26
- package/build/src/third_party/index.js +1942 -1536
- package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +21717 -20261
- package/build/src/tools/ToolDefinition.js +1 -0
- package/build/src/tools/categories.js +6 -2
- package/build/src/tools/console.js +3 -0
- package/build/src/tools/emulation.js +2 -0
- package/build/src/tools/extensions.js +6 -0
- package/build/src/tools/inPage.js +5 -35
- package/build/src/tools/input.js +13 -2
- package/build/src/tools/lighthouse.js +17 -9
- package/build/src/tools/memory.js +34 -1
- package/build/src/tools/network.js +7 -2
- package/build/src/tools/pages.js +218 -146
- package/build/src/tools/performance.js +6 -0
- package/build/src/tools/screencast.js +25 -10
- package/build/src/tools/screenshot.js +3 -0
- package/build/src/tools/script.js +2 -0
- package/build/src/tools/slim/tools.js +4 -0
- package/build/src/tools/snapshot.js +5 -1
- package/build/src/tools/tools.js +1 -0
- package/build/src/tools/webmcp.js +3 -0
- package/build/src/trace-processing/parse.js +1 -0
- package/build/src/types.js +1 -0
- package/build/src/utils/check-for-updates.js +1 -0
- package/build/src/utils/files.js +5 -10
- package/build/src/utils/id.js +1 -0
- package/build/src/utils/keyboard.js +1 -0
- package/build/src/utils/pagination.js +1 -0
- package/build/src/utils/string.js +1 -0
- package/build/src/utils/types.js +1 -0
- package/build/src/version.js +2 -1
- package/package.json +10 -9
- package/build/src/bin/cliDefinitions.js +0 -621
package/README.md
CHANGED
|
@@ -591,6 +591,10 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
591
591
|
Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.
|
|
592
592
|
- **Type:** boolean
|
|
593
593
|
|
|
594
|
+
- **`--experimentalFfmpegPath`/ `--experimental-ffmpeg-path`**
|
|
595
|
+
Path to ffmpeg executable for screencast recording.
|
|
596
|
+
- **Type:** string
|
|
597
|
+
|
|
594
598
|
- **`--experimentalWebmcp`/ `--experimental-webmcp`**
|
|
595
599
|
Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`
|
|
596
600
|
- **Type:** boolean
|
|
@@ -56,6 +56,17 @@ export class HeapSnapshotManager {
|
|
|
56
56
|
}
|
|
57
57
|
return uid;
|
|
58
58
|
}
|
|
59
|
+
async getNodesByUid(filePath, uid) {
|
|
60
|
+
const snapshot = await this.getSnapshot(filePath);
|
|
61
|
+
const filter = new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
|
|
62
|
+
const className = await this.resolveClassKeyFromUid(filePath, uid);
|
|
63
|
+
if (!className) {
|
|
64
|
+
throw new Error(`Class with UID ${uid} not found in heap snapshot`);
|
|
65
|
+
}
|
|
66
|
+
const provider = snapshot.createNodesProviderForClass(className, filter);
|
|
67
|
+
const range = await provider.serializeItemsRange(0, 1);
|
|
68
|
+
return await provider.serializeItemsRange(0, range.totalLength);
|
|
69
|
+
}
|
|
59
70
|
#getCachedSnapshot(filePath) {
|
|
60
71
|
const absolutePath = path.resolve(filePath);
|
|
61
72
|
const cached = this.#snapshots.get(absolutePath);
|
|
@@ -64,6 +75,10 @@ export class HeapSnapshotManager {
|
|
|
64
75
|
}
|
|
65
76
|
return cached;
|
|
66
77
|
}
|
|
78
|
+
async resolveClassKeyFromUid(filePath, uid) {
|
|
79
|
+
const cached = this.#getCachedSnapshot(filePath);
|
|
80
|
+
return cached.uidToClassKey.get(uid);
|
|
81
|
+
}
|
|
67
82
|
async #loadSnapshot(absolutePath) {
|
|
68
83
|
const workerProxy = new DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy(() => {
|
|
69
84
|
/* noop */
|
|
@@ -92,3 +107,4 @@ export class HeapSnapshotManager {
|
|
|
92
107
|
}
|
|
93
108
|
}
|
|
94
109
|
}
|
|
110
|
+
//# sourceMappingURL=HeapSnapshotManager.js.map
|
package/build/src/McpContext.js
CHANGED
|
@@ -4,16 +4,17 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
|
+
import os from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
10
|
import { UniverseManager } from './DevtoolsUtils.js';
|
|
9
11
|
import { HeapSnapshotManager } from './HeapSnapshotManager.js';
|
|
10
12
|
import { McpPage } from './McpPage.js';
|
|
11
13
|
import { NetworkCollector, ConsoleCollector, } from './PageCollector.js';
|
|
12
|
-
import { Locator } from './third_party/index.js';
|
|
13
|
-
import { PredefinedNetworkConditions } from './third_party/index.js';
|
|
14
|
+
import { Locator, PredefinedNetworkConditions, } from './third_party/index.js';
|
|
14
15
|
import { listPages } from './tools/pages.js';
|
|
15
16
|
import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
|
|
16
|
-
import { ensureExtension,
|
|
17
|
+
import { ensureExtension, getTempFilePath } from './utils/files.js';
|
|
17
18
|
import { getNetworkMultiplierFromString } from './WaitForHelper.js';
|
|
18
19
|
const DEFAULT_TIMEOUT = 5_000;
|
|
19
20
|
const NAVIGATION_TIMEOUT = 10_000;
|
|
@@ -37,11 +38,11 @@ export class McpContext {
|
|
|
37
38
|
#extensionPages = new WeakMap();
|
|
38
39
|
#extensionServiceWorkerMap = new WeakMap();
|
|
39
40
|
#nextExtensionServiceWorkerId = 1;
|
|
40
|
-
#nextSnapshotId = 1;
|
|
41
41
|
#traceResults = [];
|
|
42
42
|
#locatorClass;
|
|
43
43
|
#options;
|
|
44
44
|
#heapSnapshotManager = new HeapSnapshotManager();
|
|
45
|
+
#roots = undefined;
|
|
45
46
|
constructor(browser, logger, options, locatorClass) {
|
|
46
47
|
this.browser = browser;
|
|
47
48
|
this.logger = logger;
|
|
@@ -90,6 +91,39 @@ export class McpContext {
|
|
|
90
91
|
await context.#init();
|
|
91
92
|
return context;
|
|
92
93
|
}
|
|
94
|
+
roots() {
|
|
95
|
+
if (this.#roots === undefined) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return [
|
|
99
|
+
...this.#roots,
|
|
100
|
+
{
|
|
101
|
+
uri: pathToFileURL(os.tmpdir()).href,
|
|
102
|
+
name: 'temp',
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
setRoots(roots) {
|
|
107
|
+
this.#roots = roots;
|
|
108
|
+
}
|
|
109
|
+
validatePath(filePath) {
|
|
110
|
+
if (filePath === undefined) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const roots = this.roots();
|
|
114
|
+
if (roots === undefined) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const absolutePath = path.resolve(filePath);
|
|
118
|
+
for (const root of roots) {
|
|
119
|
+
const rootPath = path.resolve(fileURLToPath(root.uri));
|
|
120
|
+
if (absolutePath === rootPath ||
|
|
121
|
+
absolutePath.startsWith(rootPath + path.sep)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Access denied: path ${filePath} is not within any of the workspace roots ${JSON.stringify(roots)}.`);
|
|
126
|
+
}
|
|
93
127
|
resolveCdpRequestId(page, cdpRequestId) {
|
|
94
128
|
if (!cdpRequestId) {
|
|
95
129
|
this.logger('no network request');
|
|
@@ -105,29 +139,6 @@ export class McpContext {
|
|
|
105
139
|
}
|
|
106
140
|
return this.#networkCollector.getIdForResource(request);
|
|
107
141
|
}
|
|
108
|
-
resolveCdpElementId(page, cdpBackendNodeId) {
|
|
109
|
-
if (!cdpBackendNodeId) {
|
|
110
|
-
this.logger('no cdpBackendNodeId');
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const snapshot = page.textSnapshot;
|
|
114
|
-
if (!snapshot) {
|
|
115
|
-
this.logger('no text snapshot');
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
// TODO: index by backendNodeId instead.
|
|
119
|
-
const queue = [snapshot.root];
|
|
120
|
-
while (queue.length) {
|
|
121
|
-
const current = queue.pop();
|
|
122
|
-
if (current.backendNodeId === cdpBackendNodeId) {
|
|
123
|
-
return current.id;
|
|
124
|
-
}
|
|
125
|
-
for (const child of current.children) {
|
|
126
|
-
queue.push(child);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
142
|
getNetworkRequests(page, includePreservedRequests) {
|
|
132
143
|
return this.#networkCollector.getData(page.pptrPage, includePreservedRequests);
|
|
133
144
|
}
|
|
@@ -486,111 +497,19 @@ export class McpContext {
|
|
|
486
497
|
getIsolatedContextName(page) {
|
|
487
498
|
return this.#mcpPages.get(page)?.isolatedContextName;
|
|
488
499
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
async getDevToolsData(page) {
|
|
500
|
+
async saveTemporaryFile(data, filename) {
|
|
501
|
+
const filepath = await getTempFilePath(filename);
|
|
502
|
+
this.validatePath(filepath);
|
|
493
503
|
try {
|
|
494
|
-
|
|
495
|
-
const devtoolsPage = this.getDevToolsPage(page.pptrPage);
|
|
496
|
-
if (!devtoolsPage) {
|
|
497
|
-
this.logger('No DevTools page detected');
|
|
498
|
-
return {};
|
|
499
|
-
}
|
|
500
|
-
const { cdpRequestId, cdpBackendNodeId } = await devtoolsPage.evaluate(async () => {
|
|
501
|
-
// @ts-expect-error no types
|
|
502
|
-
const UI = await import('/bundled/ui/legacy/legacy.js');
|
|
503
|
-
// @ts-expect-error no types
|
|
504
|
-
const SDK = await import('/bundled/core/sdk/sdk.js');
|
|
505
|
-
const request = UI.Context.Context.instance().flavor(SDK.NetworkRequest.NetworkRequest);
|
|
506
|
-
const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
|
|
507
|
-
return {
|
|
508
|
-
cdpRequestId: request?.requestId(),
|
|
509
|
-
cdpBackendNodeId: node?.backendNodeId(),
|
|
510
|
-
};
|
|
511
|
-
});
|
|
512
|
-
return { cdpBackendNodeId, cdpRequestId };
|
|
504
|
+
await fs.writeFile(filepath, data);
|
|
513
505
|
}
|
|
514
506
|
catch (err) {
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
return {};
|
|
518
|
-
}
|
|
519
|
-
/**
|
|
520
|
-
* Creates a text snapshot of a page.
|
|
521
|
-
*/
|
|
522
|
-
async createTextSnapshot(page, verbose = false, devtoolsData = undefined) {
|
|
523
|
-
const rootNode = await page.pptrPage.accessibility.snapshot({
|
|
524
|
-
includeIframes: true,
|
|
525
|
-
interestingOnly: !verbose,
|
|
526
|
-
});
|
|
527
|
-
if (!rootNode) {
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
const { uniqueBackendNodeIdToMcpId } = page;
|
|
531
|
-
const snapshotId = this.#nextSnapshotId++;
|
|
532
|
-
// Iterate through the whole accessibility node tree and assign node ids that
|
|
533
|
-
// will be used for the tree serialization and mapping ids back to nodes.
|
|
534
|
-
let idCounter = 0;
|
|
535
|
-
const idToNode = new Map();
|
|
536
|
-
const seenUniqueIds = new Set();
|
|
537
|
-
const assignIds = (node) => {
|
|
538
|
-
let id = '';
|
|
539
|
-
// @ts-expect-error untyped loaderId & backendNodeId.
|
|
540
|
-
const uniqueBackendId = `${node.loaderId}_${node.backendNodeId}`;
|
|
541
|
-
if (uniqueBackendNodeIdToMcpId.has(uniqueBackendId)) {
|
|
542
|
-
// Re-use MCP exposed ID if the uniqueId is the same.
|
|
543
|
-
id = uniqueBackendNodeIdToMcpId.get(uniqueBackendId);
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
// Only generate a new ID if we have not seen the node before.
|
|
547
|
-
id = `${snapshotId}_${idCounter++}`;
|
|
548
|
-
uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
|
|
549
|
-
}
|
|
550
|
-
seenUniqueIds.add(uniqueBackendId);
|
|
551
|
-
const nodeWithId = {
|
|
552
|
-
...node,
|
|
553
|
-
id,
|
|
554
|
-
children: node.children
|
|
555
|
-
? node.children.map(child => assignIds(child))
|
|
556
|
-
: [],
|
|
557
|
-
};
|
|
558
|
-
// The AXNode for an option doesn't contain its `value`.
|
|
559
|
-
// Therefore, set text content of the option as value.
|
|
560
|
-
if (node.role === 'option') {
|
|
561
|
-
const optionText = node.name;
|
|
562
|
-
if (optionText) {
|
|
563
|
-
nodeWithId.value = optionText.toString();
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
idToNode.set(nodeWithId.id, nodeWithId);
|
|
567
|
-
return nodeWithId;
|
|
568
|
-
};
|
|
569
|
-
const rootNodeWithId = assignIds(rootNode);
|
|
570
|
-
const snapshot = {
|
|
571
|
-
root: rootNodeWithId,
|
|
572
|
-
snapshotId: String(snapshotId),
|
|
573
|
-
idToNode,
|
|
574
|
-
hasSelectedElement: false,
|
|
575
|
-
verbose,
|
|
576
|
-
};
|
|
577
|
-
page.textSnapshot = snapshot;
|
|
578
|
-
const data = devtoolsData ?? (await this.getDevToolsData(page));
|
|
579
|
-
if (data?.cdpBackendNodeId) {
|
|
580
|
-
snapshot.hasSelectedElement = true;
|
|
581
|
-
snapshot.selectedElementUid = this.resolveCdpElementId(page, data?.cdpBackendNodeId);
|
|
582
|
-
}
|
|
583
|
-
// Clean up unique IDs that we did not see anymore.
|
|
584
|
-
for (const key of uniqueBackendNodeIdToMcpId.keys()) {
|
|
585
|
-
if (!seenUniqueIds.has(key)) {
|
|
586
|
-
uniqueBackendNodeIdToMcpId.delete(key);
|
|
587
|
-
}
|
|
507
|
+
throw new Error('Could not save a file', { cause: err });
|
|
588
508
|
}
|
|
589
|
-
|
|
590
|
-
async saveTemporaryFile(data, filename) {
|
|
591
|
-
return await saveTemporaryFile(data, filename);
|
|
509
|
+
return { filepath };
|
|
592
510
|
}
|
|
593
511
|
async saveFile(data, clientProvidedFilePath, extension) {
|
|
512
|
+
this.validatePath(clientProvidedFilePath);
|
|
594
513
|
try {
|
|
595
514
|
const filePath = ensureExtension(path.resolve(clientProvidedFilePath), extension);
|
|
596
515
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
@@ -643,6 +562,7 @@ export class McpContext {
|
|
|
643
562
|
await this.#networkCollector.init(pages);
|
|
644
563
|
}
|
|
645
564
|
async installExtension(extensionPath) {
|
|
565
|
+
this.validatePath(extensionPath);
|
|
646
566
|
const id = await this.browser.installExtension(extensionPath);
|
|
647
567
|
return id;
|
|
648
568
|
}
|
|
@@ -666,12 +586,20 @@ export class McpContext {
|
|
|
666
586
|
return pptrExtensions.get(id);
|
|
667
587
|
}
|
|
668
588
|
async getHeapSnapshotAggregates(filePath) {
|
|
589
|
+
this.validatePath(filePath);
|
|
669
590
|
return await this.#heapSnapshotManager.getAggregates(filePath);
|
|
670
591
|
}
|
|
671
592
|
async getHeapSnapshotStats(filePath) {
|
|
593
|
+
this.validatePath(filePath);
|
|
672
594
|
return await this.#heapSnapshotManager.getStats(filePath);
|
|
673
595
|
}
|
|
674
596
|
async getHeapSnapshotStaticData(filePath) {
|
|
597
|
+
this.validatePath(filePath);
|
|
675
598
|
return await this.#heapSnapshotManager.getStaticData(filePath);
|
|
676
599
|
}
|
|
600
|
+
async getHeapSnapshotNodesByUid(filePath, uid) {
|
|
601
|
+
this.validatePath(filePath);
|
|
602
|
+
return await this.#heapSnapshotManager.getNodesByUid(filePath, uid);
|
|
603
|
+
}
|
|
677
604
|
}
|
|
605
|
+
//# sourceMappingURL=McpContext.js.map
|
package/build/src/McpPage.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
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';
|
|
7
9
|
import { getNetworkMultiplierFromString, WaitForHelper, } from './WaitForHelper.js';
|
|
8
10
|
/**
|
|
@@ -19,6 +21,7 @@ export class McpPage {
|
|
|
19
21
|
// Snapshot
|
|
20
22
|
textSnapshot = null;
|
|
21
23
|
uniqueBackendNodeIdToMcpId = new Map();
|
|
24
|
+
extraHandles = [];
|
|
22
25
|
// Emulation
|
|
23
26
|
emulationSettings = {};
|
|
24
27
|
// Metadata
|
|
@@ -45,6 +48,11 @@ export class McpPage {
|
|
|
45
48
|
clearDialog() {
|
|
46
49
|
this.#dialog = undefined;
|
|
47
50
|
}
|
|
51
|
+
throwIfDialogOpen() {
|
|
52
|
+
if (this.#dialog) {
|
|
53
|
+
throw new Error(`A dialog is open (${this.#dialog.type()}: ${this.#dialog.message()}).`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
48
56
|
getInPageTools() {
|
|
49
57
|
return this.inPageTools;
|
|
50
58
|
}
|
|
@@ -80,6 +88,151 @@ export class McpPage {
|
|
|
80
88
|
dispose() {
|
|
81
89
|
this.pptrPage.off('dialog', this.#dialogHandler);
|
|
82
90
|
}
|
|
91
|
+
async executeInPageTool(toolName, params, response) {
|
|
92
|
+
// Creates array of ElementHandles from the UIDs in the params.
|
|
93
|
+
// We do not replace the uids with the ElementsHandles yet, because
|
|
94
|
+
// the `evaluate` function only turns them into DOM elements if they
|
|
95
|
+
// are passed as non-nested arguments.
|
|
96
|
+
const handles = [];
|
|
97
|
+
for (const value of Object.values(params)) {
|
|
98
|
+
if (value instanceof Object &&
|
|
99
|
+
'uid' in value &&
|
|
100
|
+
typeof value.uid === 'string' &&
|
|
101
|
+
Object.keys(value).length === 1) {
|
|
102
|
+
handles.push(await this.getElementByUid(value.uid));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const result = await this.pptrPage.evaluate(async (name, args, ...elements) => {
|
|
106
|
+
// Replace the UIDs with DOM elements.
|
|
107
|
+
for (const [key, value] of Object.entries(args)) {
|
|
108
|
+
if (value instanceof Object &&
|
|
109
|
+
'uid' in value &&
|
|
110
|
+
typeof value.uid === 'string' &&
|
|
111
|
+
Object.keys(value).length === 1) {
|
|
112
|
+
args[key] = elements.shift();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!window.__dtmcp?.executeTool) {
|
|
116
|
+
throw new Error('No tools found on the page');
|
|
117
|
+
}
|
|
118
|
+
const toolResult = await window.__dtmcp.executeTool(name, args);
|
|
119
|
+
const stashDOMElement = (el) => {
|
|
120
|
+
if (!window.__dtmcp) {
|
|
121
|
+
window.__dtmcp = {};
|
|
122
|
+
}
|
|
123
|
+
if (window.__dtmcp.stashedElements === undefined) {
|
|
124
|
+
window.__dtmcp.stashedElements = [];
|
|
125
|
+
}
|
|
126
|
+
window.__dtmcp.stashedElements.push(el);
|
|
127
|
+
return {
|
|
128
|
+
stashedId: `stashed-${window.__dtmcp.stashedElements.length - 1}`,
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
const ancestors = [];
|
|
132
|
+
// Recursively walks the tool result:
|
|
133
|
+
// - Replaces DOM elements with an ID and stashes the DOM element on the window object
|
|
134
|
+
// - Replaces non-plain objects with a string representation of the object
|
|
135
|
+
// - Replaces circular references with the string '<Circular reference>'
|
|
136
|
+
// - Replaces functions with the string '<Function object>'
|
|
137
|
+
const processToolResult = (data, parentEl) => {
|
|
138
|
+
// 1. Handle DOM Elements
|
|
139
|
+
if (data instanceof Element) {
|
|
140
|
+
return stashDOMElement(data);
|
|
141
|
+
}
|
|
142
|
+
// 2. Handle Arrays
|
|
143
|
+
if (Array.isArray(data)) {
|
|
144
|
+
return data.map((item) => processToolResult(item, parentEl));
|
|
145
|
+
}
|
|
146
|
+
// 3. Handle Objects
|
|
147
|
+
if (data !== null && typeof data === 'object') {
|
|
148
|
+
while (ancestors.length > 0 && ancestors.at(-1) !== parentEl) {
|
|
149
|
+
ancestors.pop();
|
|
150
|
+
}
|
|
151
|
+
if (ancestors.includes(data)) {
|
|
152
|
+
return '<Circular reference>';
|
|
153
|
+
}
|
|
154
|
+
ancestors.push(data);
|
|
155
|
+
// If not a plain object, return a string representation of the object
|
|
156
|
+
if (Object.getPrototypeOf(data) !== Object.prototype) {
|
|
157
|
+
return `<${data.constructor.name} instance>`;
|
|
158
|
+
}
|
|
159
|
+
const processedObj = {};
|
|
160
|
+
for (const [key, value] of Object.entries(data)) {
|
|
161
|
+
processedObj[key] = processToolResult(value, data);
|
|
162
|
+
}
|
|
163
|
+
return processedObj;
|
|
164
|
+
}
|
|
165
|
+
// 4. Handle Functions
|
|
166
|
+
if (typeof data === 'function') {
|
|
167
|
+
return '<Function object>';
|
|
168
|
+
}
|
|
169
|
+
// 5. Return primitives (strings, numbers, booleans) as-is
|
|
170
|
+
return data;
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
result: processToolResult(toolResult),
|
|
174
|
+
stashed: window.__dtmcp?.stashedElements?.length ?? 0,
|
|
175
|
+
};
|
|
176
|
+
}, toolName, params, ...handles);
|
|
177
|
+
const elementHandles = [];
|
|
178
|
+
for (let i = 0; i < (result.stashed ?? 0); i++) {
|
|
179
|
+
const elementHandle = await this.pptrPage.evaluateHandle(index => {
|
|
180
|
+
const el = window.__dtmcp?.stashedElements?.[index];
|
|
181
|
+
if (!el) {
|
|
182
|
+
throw new Error(`Stashed element at index ${index} not found`);
|
|
183
|
+
}
|
|
184
|
+
return el;
|
|
185
|
+
}, i);
|
|
186
|
+
elementHandles.push(elementHandle);
|
|
187
|
+
}
|
|
188
|
+
if (elementHandles.length) {
|
|
189
|
+
const oldHandles = [...this.extraHandles];
|
|
190
|
+
this.textSnapshot = await TextSnapshot.create(this, {
|
|
191
|
+
extraHandles: elementHandles,
|
|
192
|
+
});
|
|
193
|
+
response.includeSnapshot();
|
|
194
|
+
for (const handle of oldHandles) {
|
|
195
|
+
await handle
|
|
196
|
+
.dispose()
|
|
197
|
+
.catch(e => logger('Failed to dispose old handle', e));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const cdpElementIds = await Promise.all(elementHandles.map(async (elementHandle, index) => {
|
|
201
|
+
const backendNodeId = await elementHandle.backendNodeId();
|
|
202
|
+
if (!backendNodeId) {
|
|
203
|
+
logger(`No backendNodeId for stashed DOM element with index ${index}`);
|
|
204
|
+
return `stashed-${index}`;
|
|
205
|
+
}
|
|
206
|
+
const cdpElementId = this.resolveCdpElementId(backendNodeId);
|
|
207
|
+
if (!cdpElementId) {
|
|
208
|
+
logger(`Could not get cdpElementId for backend node ${backendNodeId}`);
|
|
209
|
+
return `stashed-${index}`;
|
|
210
|
+
}
|
|
211
|
+
return cdpElementId;
|
|
212
|
+
}));
|
|
213
|
+
const recursivelyReplaceStashedElements = (node) => {
|
|
214
|
+
if (Array.isArray(node)) {
|
|
215
|
+
return node.map(x => recursivelyReplaceStashedElements(x));
|
|
216
|
+
}
|
|
217
|
+
if (node !== null && typeof node === 'object') {
|
|
218
|
+
if ('stashedId' in node &&
|
|
219
|
+
typeof node.stashedId === 'string' &&
|
|
220
|
+
node.stashedId.startsWith('stashed-') &&
|
|
221
|
+
Object.keys(node).length === 1) {
|
|
222
|
+
const index = parseInt(node.stashedId.split('-')[1]);
|
|
223
|
+
return { uid: cdpElementIds[index] };
|
|
224
|
+
}
|
|
225
|
+
const resultObj = {};
|
|
226
|
+
for (const [key, value] of Object.entries(node)) {
|
|
227
|
+
resultObj[key] = recursivelyReplaceStashedElements(value);
|
|
228
|
+
}
|
|
229
|
+
return resultObj;
|
|
230
|
+
}
|
|
231
|
+
return node;
|
|
232
|
+
};
|
|
233
|
+
const resultWithUids = recursivelyReplaceStashedElements(result.result);
|
|
234
|
+
response.appendResponseLine(JSON.stringify(resultWithUids, null, 2));
|
|
235
|
+
}
|
|
83
236
|
async getElementByUid(uid) {
|
|
84
237
|
if (!this.textSnapshot) {
|
|
85
238
|
throw new Error(`No snapshot found for page ${this.id ?? '?'}. Use ${takeSnapshot.name} to capture one.`);
|
|
@@ -108,4 +261,55 @@ export class McpPage {
|
|
|
108
261
|
getAXNodeByUid(uid) {
|
|
109
262
|
return this.textSnapshot?.idToNode.get(uid);
|
|
110
263
|
}
|
|
264
|
+
resolveCdpElementId(cdpBackendNodeId) {
|
|
265
|
+
if (!cdpBackendNodeId) {
|
|
266
|
+
logger('no cdpBackendNodeId');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const snapshot = this.textSnapshot;
|
|
270
|
+
if (!snapshot) {
|
|
271
|
+
logger('no text snapshot');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// TODO: index by backendNodeId instead.
|
|
275
|
+
const queue = [snapshot.root];
|
|
276
|
+
while (queue.length) {
|
|
277
|
+
const current = queue.pop();
|
|
278
|
+
if (current.backendNodeId === cdpBackendNodeId) {
|
|
279
|
+
return current.id;
|
|
280
|
+
}
|
|
281
|
+
for (const child of current.children) {
|
|
282
|
+
queue.push(child);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
async getDevToolsData() {
|
|
288
|
+
try {
|
|
289
|
+
logger('Getting DevTools UI data');
|
|
290
|
+
const devtoolsPage = this.devToolsPage;
|
|
291
|
+
if (!devtoolsPage) {
|
|
292
|
+
logger('No DevTools page detected');
|
|
293
|
+
return {};
|
|
294
|
+
}
|
|
295
|
+
const { cdpRequestId, cdpBackendNodeId } = await devtoolsPage.evaluate(async () => {
|
|
296
|
+
// @ts-expect-error no types
|
|
297
|
+
const UI = await import('/bundled/ui/legacy/legacy.js');
|
|
298
|
+
// @ts-expect-error no types
|
|
299
|
+
const SDK = await import('/bundled/core/sdk/sdk.js');
|
|
300
|
+
const request = UI.Context.Context.instance().flavor(SDK.NetworkRequest.NetworkRequest);
|
|
301
|
+
const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
|
|
302
|
+
return {
|
|
303
|
+
cdpRequestId: request?.requestId(),
|
|
304
|
+
cdpBackendNodeId: node?.backendNodeId(),
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
return { cdpBackendNodeId, cdpRequestId };
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
logger('error getting devtools data', err);
|
|
311
|
+
}
|
|
312
|
+
return {};
|
|
313
|
+
}
|
|
111
314
|
}
|
|
315
|
+
//# sourceMappingURL=McpPage.js.map
|
package/build/src/McpResponse.js
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { ConsoleFormatter } from './formatters/ConsoleFormatter.js';
|
|
7
7
|
import { HeapSnapshotFormatter } from './formatters/HeapSnapshotFormatter.js';
|
|
8
|
+
import { isNodeLike } from './formatters/HeapSnapshotFormatter.js';
|
|
8
9
|
import { IssueFormatter } from './formatters/IssueFormatter.js';
|
|
9
10
|
import { NetworkFormatter } from './formatters/NetworkFormatter.js';
|
|
10
11
|
import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
|
|
11
12
|
import { UncaughtError } from './PageCollector.js';
|
|
13
|
+
import { TextSnapshot } from './TextSnapshot.js';
|
|
12
14
|
import { DevTools } from './third_party/index.js';
|
|
13
15
|
import { handleDialog } from './tools/pages.js';
|
|
14
16
|
import { getInsightOutput, getTraceSummary } from './trace-processing/parse.js';
|
|
@@ -130,6 +132,7 @@ export class McpResponse {
|
|
|
130
132
|
#args;
|
|
131
133
|
#page;
|
|
132
134
|
#redactNetworkHeaders = true;
|
|
135
|
+
#error;
|
|
133
136
|
constructor(args) {
|
|
134
137
|
this.#args = args;
|
|
135
138
|
}
|
|
@@ -161,7 +164,7 @@ export class McpResponse {
|
|
|
161
164
|
this.#listExtensions = true;
|
|
162
165
|
}
|
|
163
166
|
setListInPageTools() {
|
|
164
|
-
if (this.#args.
|
|
167
|
+
if (this.#args.categoryExperimentalInPage) {
|
|
165
168
|
this.#listInPageTools = true;
|
|
166
169
|
}
|
|
167
170
|
}
|
|
@@ -203,6 +206,9 @@ export class McpResponse {
|
|
|
203
206
|
includePreservedMessages: options?.includePreservedMessages,
|
|
204
207
|
};
|
|
205
208
|
}
|
|
209
|
+
setError(error) {
|
|
210
|
+
this.#error = error;
|
|
211
|
+
}
|
|
206
212
|
attachNetworkRequest(reqId, options) {
|
|
207
213
|
this.#attachedNetworkRequestId = reqId;
|
|
208
214
|
this.#attachedNetworkRequestOptions = options;
|
|
@@ -253,6 +259,9 @@ export class McpResponse {
|
|
|
253
259
|
get consoleMessagesTypes() {
|
|
254
260
|
return this.#consoleDataOptions?.types;
|
|
255
261
|
}
|
|
262
|
+
get error() {
|
|
263
|
+
return this.#error;
|
|
264
|
+
}
|
|
256
265
|
appendResponseLine(value) {
|
|
257
266
|
this.#textResponseLines.push(value);
|
|
258
267
|
}
|
|
@@ -272,6 +281,14 @@ export class McpResponse {
|
|
|
272
281
|
staticData,
|
|
273
282
|
};
|
|
274
283
|
}
|
|
284
|
+
setHeapSnapshotNodes(nodes, options) {
|
|
285
|
+
this.#heapSnapshotOptions = {
|
|
286
|
+
...this.#heapSnapshotOptions,
|
|
287
|
+
include: true,
|
|
288
|
+
nodes,
|
|
289
|
+
pagination: options,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
275
292
|
attachImage(value) {
|
|
276
293
|
this.#images.push(value);
|
|
277
294
|
}
|
|
@@ -299,7 +316,10 @@ export class McpResponse {
|
|
|
299
316
|
if (!this.#page) {
|
|
300
317
|
throw new Error('Response must have a page');
|
|
301
318
|
}
|
|
302
|
-
await
|
|
319
|
+
this.#page.textSnapshot = await TextSnapshot.create(this.#page, {
|
|
320
|
+
verbose: this.#snapshotParams.verbose,
|
|
321
|
+
devtoolsData: this.#devToolsData,
|
|
322
|
+
});
|
|
303
323
|
const textSnapshot = this.#page.textSnapshot;
|
|
304
324
|
if (textSnapshot) {
|
|
305
325
|
const formatter = new SnapshotFormatter(textSnapshot);
|
|
@@ -349,7 +369,7 @@ export class McpResponse {
|
|
|
349
369
|
const formatter = new IssueFormatter(message, {
|
|
350
370
|
id: consoleMessageStableId,
|
|
351
371
|
requestIdResolver: context.resolveCdpRequestId.bind(context, this.#page),
|
|
352
|
-
elementIdResolver:
|
|
372
|
+
elementIdResolver: this.#page.resolveCdpElementId.bind(this.#page),
|
|
353
373
|
});
|
|
354
374
|
if (!formatter.isValid()) {
|
|
355
375
|
throw new Error("Can't provide details for the msgid " + consoleMessageStableId);
|
|
@@ -451,6 +471,7 @@ export class McpResponse {
|
|
|
451
471
|
lighthouseResult: this.#attachedLighthouseResult,
|
|
452
472
|
inPageTools,
|
|
453
473
|
webmcpTools,
|
|
474
|
+
errorMessage: this.#error?.message,
|
|
454
475
|
});
|
|
455
476
|
}
|
|
456
477
|
format(toolName, context, data) {
|
|
@@ -641,6 +662,17 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
641
662
|
response.push(formatter.toString());
|
|
642
663
|
structuredContent.heapSnapshotData = formatter.toJSON();
|
|
643
664
|
}
|
|
665
|
+
const nodes = this.#heapSnapshotOptions.nodes;
|
|
666
|
+
if (nodes) {
|
|
667
|
+
const sortedItems = nodes.items
|
|
668
|
+
.filter(isNodeLike)
|
|
669
|
+
.sort((a, b) => b.retainedSize - a.retainedSize);
|
|
670
|
+
const paginationData = this.#dataWithPagination(sortedItems, this.#heapSnapshotOptions.pagination);
|
|
671
|
+
response.push(HeapSnapshotFormatter.formatNodes(paginationData.items));
|
|
672
|
+
structuredContent.pagination = paginationData.pagination;
|
|
673
|
+
response.push(...paginationData.info);
|
|
674
|
+
structuredContent.heapSnapshotNodes = paginationData.items;
|
|
675
|
+
}
|
|
644
676
|
}
|
|
645
677
|
if (data.detailedNetworkRequest) {
|
|
646
678
|
response.push(data.detailedNetworkRequest.toStringDetailed());
|
|
@@ -729,16 +761,21 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
729
761
|
const messages = data.consoleMessages ?? [];
|
|
730
762
|
response.push('## Console messages');
|
|
731
763
|
if (messages.length) {
|
|
732
|
-
const
|
|
764
|
+
const grouped = ConsoleFormatter.groupConsecutive(messages);
|
|
765
|
+
const paginationData = this.#dataWithPagination(grouped, this.#consoleDataOptions.pagination);
|
|
733
766
|
structuredContent.pagination = paginationData.pagination;
|
|
734
767
|
response.push(...paginationData.info);
|
|
735
|
-
response.push(...paginationData.items.map(
|
|
736
|
-
structuredContent.consoleMessages = paginationData.items.map(
|
|
768
|
+
response.push(...paginationData.items.map(item => item.toString()));
|
|
769
|
+
structuredContent.consoleMessages = paginationData.items.map(item => item.toJSON());
|
|
737
770
|
}
|
|
738
771
|
else {
|
|
739
772
|
response.push('<no console messages found>');
|
|
740
773
|
}
|
|
741
774
|
}
|
|
775
|
+
if (data.errorMessage) {
|
|
776
|
+
response.push(`Error: ${data.errorMessage}`);
|
|
777
|
+
structuredContent.errorMessage = data.errorMessage;
|
|
778
|
+
}
|
|
742
779
|
const text = {
|
|
743
780
|
type: 'text',
|
|
744
781
|
text: response.join('\n'),
|
|
@@ -800,3 +837,4 @@ function createStructuredPage(page, context) {
|
|
|
800
837
|
}
|
|
801
838
|
return entry;
|
|
802
839
|
}
|
|
840
|
+
//# sourceMappingURL=McpResponse.js.map
|
package/build/src/Mutex.js
CHANGED