chrome-devtools-mcp 0.2.0 → 0.2.2
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 +27 -8
- package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Progress.js +60 -53
- package/build/node_modules/chrome-devtools-frontend/front_end/core/host/GdpClient.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +5 -2
- package/build/node_modules/chrome-devtools-frontend/front_end/core/protocol_client/InspectorBackend.js +2 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +11 -10
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSModel.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSPropertyParserMatchers.js +24 -4
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/DebuggerModel.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/EnhancedTracesParser.js +29 -24
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js +9 -15
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RemoteObject.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ResourceTreeModel.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RuntimeModel.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ServiceWorkerManager.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMap.js +4 -31
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/TraceObject.js +5 -2
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.js +6 -4
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +259 -179
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/UnitFormatters.js +10 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js +14 -3
- package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ContentProviderBasedProject.js +6 -4
- package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +2 -2
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/ModelImpl.js +4 -9
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/Processor.js +17 -9
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/AuctionWorkletsHandler.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/FramesHandler.js +2 -2
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/LayoutShiftsHandler.js +3 -4
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/MetaHandler.js +10 -9
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/ScreenshotsHandler.js +0 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/ScriptsHandler.js +4 -4
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserInteractionsHandler.js +2 -10
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserTimingsHandler.js +3 -4
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/SamplesIntegrator.js +8 -6
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/CLSCulprits.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DocumentLatency.js +3 -3
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DuplicatedJavaScript.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/INPBreakdown.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/ImageDelivery.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LCPBreakdown.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LCPDiscovery.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/ModernHTTP.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/NetworkDependencyTree.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/RenderBlocking.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js +21 -21
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/SourceMapsResolver.js +5 -3
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/trace_source_maps_resolver.js +1 -1
- package/build/src/McpContext.js +60 -10
- package/build/src/McpResponse.js +5 -4
- package/build/src/WaitForHelper.js +127 -0
- package/build/src/browser.js +12 -9
- package/build/src/index.js +20 -21
- package/build/src/logger.js +1 -0
- package/build/src/tools/input.js +5 -6
- package/build/src/tools/pages.js +2 -3
- package/build/src/tools/performance.js +16 -14
- package/build/src/tools/script.js +1 -2
- package/build/src/trace-processing/parse.js +23 -14
- package/package.json +15 -16
- package/build/src/waitForHelpers.js +0 -109
package/build/src/McpContext.js
CHANGED
|
@@ -3,6 +3,23 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { listPages } from './tools/pages.js';
|
|
6
|
+
import { WaitForHelper } from './WaitForHelper.js';
|
|
7
|
+
const DEFAULT_TIMEOUT = 5_000;
|
|
8
|
+
const NAVIGATION_TIMEOUT = 10_000;
|
|
9
|
+
function getNetworkMultiplierFromString(condition) {
|
|
10
|
+
const puppeteerCondition = condition;
|
|
11
|
+
switch (puppeteerCondition) {
|
|
12
|
+
case 'Fast 4G':
|
|
13
|
+
return 1;
|
|
14
|
+
case 'Slow 4G':
|
|
15
|
+
return 2.5;
|
|
16
|
+
case 'Fast 3G':
|
|
17
|
+
return 5;
|
|
18
|
+
case 'Slow 3G':
|
|
19
|
+
return 10;
|
|
20
|
+
}
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
6
23
|
export class McpContext {
|
|
7
24
|
browser;
|
|
8
25
|
logger;
|
|
@@ -14,8 +31,8 @@ export class McpContext {
|
|
|
14
31
|
#networkCollector;
|
|
15
32
|
#consoleCollector;
|
|
16
33
|
#isRunningTrace = false;
|
|
17
|
-
#
|
|
18
|
-
#
|
|
34
|
+
#networkConditionsMap = new WeakMap();
|
|
35
|
+
#cpuThrottlingRateMap = new WeakMap();
|
|
19
36
|
#dialog;
|
|
20
37
|
#nextSnapshotId = 1;
|
|
21
38
|
#traceResults = [];
|
|
@@ -28,10 +45,10 @@ export class McpContext {
|
|
|
28
45
|
});
|
|
29
46
|
});
|
|
30
47
|
this.#consoleCollector = new PageCollector(this.browser, (page, collect) => {
|
|
31
|
-
page.on('console',
|
|
48
|
+
page.on('console', event => {
|
|
32
49
|
collect(event);
|
|
33
50
|
});
|
|
34
|
-
page.on('pageerror',
|
|
51
|
+
page.on('pageerror', event => {
|
|
35
52
|
collect(event);
|
|
36
53
|
});
|
|
37
54
|
});
|
|
@@ -76,16 +93,27 @@ export class McpContext {
|
|
|
76
93
|
throw new Error('Request not found for selected page');
|
|
77
94
|
}
|
|
78
95
|
setNetworkConditions(conditions) {
|
|
79
|
-
|
|
96
|
+
const page = this.getSelectedPage();
|
|
97
|
+
if (conditions === null) {
|
|
98
|
+
this.#networkConditionsMap.delete(page);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
this.#networkConditionsMap.set(page, conditions);
|
|
102
|
+
}
|
|
103
|
+
this.#updateSelectedPageTimeouts();
|
|
80
104
|
}
|
|
81
105
|
getNetworkConditions() {
|
|
82
|
-
|
|
106
|
+
const page = this.getSelectedPage();
|
|
107
|
+
return this.#networkConditionsMap.get(page) ?? null;
|
|
83
108
|
}
|
|
84
109
|
setCpuThrottlingRate(rate) {
|
|
85
|
-
|
|
110
|
+
const page = this.getSelectedPage();
|
|
111
|
+
this.#cpuThrottlingRateMap.set(page, rate);
|
|
112
|
+
this.#updateSelectedPageTimeouts();
|
|
86
113
|
}
|
|
87
114
|
getCpuThrottlingRate() {
|
|
88
|
-
|
|
115
|
+
const page = this.getSelectedPage();
|
|
116
|
+
return this.#cpuThrottlingRateMap.get(page) ?? 1;
|
|
89
117
|
}
|
|
90
118
|
setIsRunningPerformanceTrace(x) {
|
|
91
119
|
this.#isRunningTrace = x;
|
|
@@ -129,11 +157,23 @@ export class McpContext {
|
|
|
129
157
|
this.#selectedPageIdx = idx;
|
|
130
158
|
const newPage = this.getSelectedPage();
|
|
131
159
|
newPage.on('dialog', this.#dialogHandler);
|
|
160
|
+
this.#updateSelectedPageTimeouts();
|
|
161
|
+
}
|
|
162
|
+
#updateSelectedPageTimeouts() {
|
|
163
|
+
const page = this.getSelectedPage();
|
|
132
164
|
// For waiters 5sec timeout should be sufficient.
|
|
133
|
-
|
|
165
|
+
// Increased in case we throttle the CPU
|
|
166
|
+
const cpuMultiplier = this.getCpuThrottlingRate();
|
|
167
|
+
page.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
|
|
134
168
|
// 10sec should be enough for the load event to be emitted during
|
|
135
169
|
// navigations.
|
|
136
|
-
|
|
170
|
+
// Increased in case we throttle the network requests
|
|
171
|
+
const networkMultiplier = getNetworkMultiplierFromString(this.getNetworkConditions());
|
|
172
|
+
page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier);
|
|
173
|
+
}
|
|
174
|
+
getNavigationTimeout() {
|
|
175
|
+
const page = this.getSelectedPage();
|
|
176
|
+
return page.getDefaultNavigationTimeout();
|
|
137
177
|
}
|
|
138
178
|
async getElementByUid(uid) {
|
|
139
179
|
if (!this.#textSnapshot?.idToNode.size) {
|
|
@@ -216,4 +256,14 @@ export class McpContext {
|
|
|
216
256
|
recordedTraces() {
|
|
217
257
|
return this.#traceResults;
|
|
218
258
|
}
|
|
259
|
+
getWaitForHelper(page, cpuMultiplier, networkMultiplier) {
|
|
260
|
+
return new WaitForHelper(page, cpuMultiplier, networkMultiplier);
|
|
261
|
+
}
|
|
262
|
+
waitForEventsAfterAction(action) {
|
|
263
|
+
const page = this.getSelectedPage();
|
|
264
|
+
const cpuMultiplier = this.getCpuThrottlingRate();
|
|
265
|
+
const networkMultiplier = getNetworkMultiplierFromString(this.getNetworkConditions());
|
|
266
|
+
const waitForHelper = this.getWaitForHelper(page, cpuMultiplier, networkMultiplier);
|
|
267
|
+
return waitForHelper.waitForEventsAfterAction(action);
|
|
268
|
+
}
|
|
219
269
|
}
|
package/build/src/McpResponse.js
CHANGED
|
@@ -8,7 +8,7 @@ export class McpResponse {
|
|
|
8
8
|
#attachedNetworkRequestUrl;
|
|
9
9
|
#includeConsoleData = false;
|
|
10
10
|
#textResponseLines = [];
|
|
11
|
-
#
|
|
11
|
+
#formattedConsoleData;
|
|
12
12
|
#images = [];
|
|
13
13
|
setIncludePages(value) {
|
|
14
14
|
this.#includePages = value;
|
|
@@ -64,7 +64,7 @@ export class McpResponse {
|
|
|
64
64
|
const consoleMessages = context.getConsoleData();
|
|
65
65
|
if (consoleMessages) {
|
|
66
66
|
formattedConsoleMessages = await Promise.all(consoleMessages.map(message => formatConsoleEvent(message)));
|
|
67
|
-
this.#
|
|
67
|
+
this.#formattedConsoleData = formattedConsoleMessages;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
return this.format(toolName, context);
|
|
@@ -78,6 +78,7 @@ export class McpResponse {
|
|
|
78
78
|
if (networkConditions) {
|
|
79
79
|
response.push(`## Network emulation`);
|
|
80
80
|
response.push(`Emulating: ${networkConditions}`);
|
|
81
|
+
response.push(`Navigation timeout set to ${context.getNavigationTimeout()} ms`);
|
|
81
82
|
}
|
|
82
83
|
const cpuThrottlingRate = context.getCpuThrottlingRate();
|
|
83
84
|
if (cpuThrottlingRate > 1) {
|
|
@@ -120,9 +121,9 @@ Call browser_handle_dialog to handle it before continuing.`);
|
|
|
120
121
|
response.push('No requests found.');
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
|
-
if (this.#includeConsoleData && this.#
|
|
124
|
+
if (this.#includeConsoleData && this.#formattedConsoleData) {
|
|
124
125
|
response.push('## Console messages');
|
|
125
|
-
response.push(...this.#
|
|
126
|
+
response.push(...this.#formattedConsoleData);
|
|
126
127
|
}
|
|
127
128
|
const text = {
|
|
128
129
|
type: 'text',
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
export class WaitForHelper {
|
|
3
|
+
#abortController = new AbortController();
|
|
4
|
+
#page;
|
|
5
|
+
#stableDomTimeout;
|
|
6
|
+
#stableDomFor;
|
|
7
|
+
#expectNavigationIn;
|
|
8
|
+
#navigationTimeout;
|
|
9
|
+
constructor(page, cpuTimeoutMultiplier, networkTimeoutMultiplier) {
|
|
10
|
+
this.#stableDomTimeout = 3000 * cpuTimeoutMultiplier;
|
|
11
|
+
this.#stableDomFor = 100 * cpuTimeoutMultiplier;
|
|
12
|
+
this.#expectNavigationIn = 100 * cpuTimeoutMultiplier;
|
|
13
|
+
this.#navigationTimeout = 3000 * networkTimeoutMultiplier;
|
|
14
|
+
this.#page = page;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A wrapper that executes a action and waits for
|
|
18
|
+
* a potential navigation, after which it waits
|
|
19
|
+
* for the DOM to be stable before returning.
|
|
20
|
+
*/
|
|
21
|
+
async waitForStableDom() {
|
|
22
|
+
const stableDomObserver = await this.#page.evaluateHandle(timeout => {
|
|
23
|
+
let timeoutId;
|
|
24
|
+
function callback() {
|
|
25
|
+
clearTimeout(timeoutId);
|
|
26
|
+
timeoutId = setTimeout(() => {
|
|
27
|
+
domObserver.resolver.resolve();
|
|
28
|
+
domObserver.observer.disconnect();
|
|
29
|
+
}, timeout);
|
|
30
|
+
}
|
|
31
|
+
const domObserver = {
|
|
32
|
+
resolver: Promise.withResolvers(),
|
|
33
|
+
observer: new MutationObserver(callback),
|
|
34
|
+
};
|
|
35
|
+
// It's possible that the DOM is not gonna change so we
|
|
36
|
+
// need to start the timeout initially.
|
|
37
|
+
callback();
|
|
38
|
+
domObserver.observer.observe(document.body, {
|
|
39
|
+
childList: true,
|
|
40
|
+
subtree: true,
|
|
41
|
+
attributes: true,
|
|
42
|
+
});
|
|
43
|
+
return domObserver;
|
|
44
|
+
}, this.#stableDomFor);
|
|
45
|
+
this.#abortController.signal.addEventListener('abort', async () => {
|
|
46
|
+
try {
|
|
47
|
+
await stableDomObserver.evaluate(observer => {
|
|
48
|
+
observer.observer.disconnect();
|
|
49
|
+
observer.resolver.resolve();
|
|
50
|
+
});
|
|
51
|
+
await stableDomObserver.dispose();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Ignored cleanup errors
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return Promise.race([
|
|
58
|
+
stableDomObserver.evaluate(async (observer) => {
|
|
59
|
+
return await observer.resolver.promise;
|
|
60
|
+
}),
|
|
61
|
+
this.timeout(this.#stableDomTimeout).then(() => {
|
|
62
|
+
throw new Error('Timeout');
|
|
63
|
+
}),
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
async waitForNavigationStarted() {
|
|
67
|
+
// Currently Puppeteer does not have API
|
|
68
|
+
// For when a navigation is about to start
|
|
69
|
+
const navigationStartedPromise = new Promise(resolve => {
|
|
70
|
+
const listener = (event) => {
|
|
71
|
+
if ([
|
|
72
|
+
'historySameDocument',
|
|
73
|
+
'historyDifferentDocument',
|
|
74
|
+
'sameDocument',
|
|
75
|
+
].includes(event.navigationType)) {
|
|
76
|
+
resolve(false);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
resolve(true);
|
|
80
|
+
};
|
|
81
|
+
this.#page._client().on('Page.frameStartedNavigating', listener);
|
|
82
|
+
this.#abortController.signal.addEventListener('abort', () => {
|
|
83
|
+
resolve(false);
|
|
84
|
+
this.#page._client().off('Page.frameStartedNavigating', listener);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
return await Promise.race([
|
|
88
|
+
navigationStartedPromise,
|
|
89
|
+
this.timeout(this.#expectNavigationIn).then(() => false),
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
timeout(time) {
|
|
93
|
+
return new Promise(res => {
|
|
94
|
+
const id = setTimeout(res, time);
|
|
95
|
+
this.#abortController.signal.addEventListener('abort', () => {
|
|
96
|
+
res();
|
|
97
|
+
clearTimeout(id);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async waitForEventsAfterAction(action) {
|
|
102
|
+
const navigationFinished = this.waitForNavigationStarted()
|
|
103
|
+
.then(navigationStated => {
|
|
104
|
+
if (navigationStated) {
|
|
105
|
+
return this.#page.waitForNavigation({
|
|
106
|
+
timeout: this.#navigationTimeout,
|
|
107
|
+
signal: this.#abortController.signal,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
})
|
|
112
|
+
.catch(error => logger(error));
|
|
113
|
+
await action();
|
|
114
|
+
try {
|
|
115
|
+
await navigationFinished;
|
|
116
|
+
// Wait for stable dom after navigation so we execute in
|
|
117
|
+
// the correct context
|
|
118
|
+
await this.waitForStableDom();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
logger(error);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
this.#abortController.abort();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
package/build/src/browser.js
CHANGED
|
@@ -53,11 +53,7 @@ export async function launch(options) {
|
|
|
53
53
|
recursive: true,
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
-
const args = [
|
|
57
|
-
'--remote-debugging-pipe',
|
|
58
|
-
'--no-first-run',
|
|
59
|
-
'--hide-crash-restore-bubble',
|
|
60
|
-
];
|
|
56
|
+
const args = ['--hide-crash-restore-bubble'];
|
|
61
57
|
if (customDevTools) {
|
|
62
58
|
args.push(`--custom-devtools-frontend=file://${customDevTools}`);
|
|
63
59
|
}
|
|
@@ -69,7 +65,7 @@ export async function launch(options) {
|
|
|
69
65
|
: 'chrome';
|
|
70
66
|
}
|
|
71
67
|
try {
|
|
72
|
-
|
|
68
|
+
const browser = await puppeteer.launch({
|
|
73
69
|
...connectOptions,
|
|
74
70
|
channel: puppeterChannel,
|
|
75
71
|
executablePath,
|
|
@@ -79,12 +75,19 @@ export async function launch(options) {
|
|
|
79
75
|
headless,
|
|
80
76
|
args,
|
|
81
77
|
});
|
|
78
|
+
if (options.logFile) {
|
|
79
|
+
// FIXME: we are probably subscribing too late to catch startup logs. We
|
|
80
|
+
// should expose the process earlier or expose the getRecentLogs() getter.
|
|
81
|
+
browser.process()?.stderr?.pipe(options.logFile);
|
|
82
|
+
browser.process()?.stdout?.pipe(options.logFile);
|
|
83
|
+
}
|
|
84
|
+
return browser;
|
|
82
85
|
}
|
|
83
86
|
catch (error) {
|
|
84
|
-
// TODO: check browser logs for `Failed to create a ProcessSingleton for
|
|
85
|
-
// your profile directory` instead.
|
|
86
87
|
if (userDataDir &&
|
|
87
|
-
error.message.includes('The browser is already running')
|
|
88
|
+
(error.message.includes('The browser is already running') ||
|
|
89
|
+
error.message.includes('Target closed') ||
|
|
90
|
+
error.message.includes('Connection closed'))) {
|
|
88
91
|
throw new Error(`The browser is already running for ${userDataDir}. Use --isolated to run multiple browser instances.`, {
|
|
89
92
|
cause: error,
|
|
90
93
|
});
|
package/build/src/index.js
CHANGED
|
@@ -61,10 +61,9 @@ export const cliOptions = {
|
|
|
61
61
|
},
|
|
62
62
|
channel: {
|
|
63
63
|
type: 'string',
|
|
64
|
-
description: 'Specify a different Chrome channel that should be used.',
|
|
64
|
+
description: 'Specify a different Chrome channel that should be used. The default is the stable channel version.',
|
|
65
65
|
choices: ['stable', 'canary', 'beta', 'dev'],
|
|
66
66
|
conflicts: ['browserUrl', 'executablePath'],
|
|
67
|
-
default: 'stable',
|
|
68
67
|
},
|
|
69
68
|
logFile: {
|
|
70
69
|
type: 'string',
|
|
@@ -72,6 +71,22 @@ export const cliOptions = {
|
|
|
72
71
|
hidden: true,
|
|
73
72
|
},
|
|
74
73
|
};
|
|
74
|
+
function readPackageJson() {
|
|
75
|
+
const currentDir = import.meta.dirname;
|
|
76
|
+
const packageJsonPath = path.join(currentDir, '..', '..', 'package.json');
|
|
77
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const json = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
82
|
+
assert.strict(json['name'], 'chrome-devtools-mcp');
|
|
83
|
+
return json;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const version = readPackageJson().version ?? 'unknown';
|
|
75
90
|
const yargsInstance = yargs(hideBin(process.argv))
|
|
76
91
|
.scriptName('npx chrome-devtools-mcp@latest')
|
|
77
92
|
.options(cliOptions)
|
|
@@ -98,26 +113,9 @@ const yargsInstance = yargs(hideBin(process.argv))
|
|
|
98
113
|
export const args = yargsInstance
|
|
99
114
|
.wrap(Math.min(120, yargsInstance.terminalWidth()))
|
|
100
115
|
.help()
|
|
116
|
+
.version(version)
|
|
101
117
|
.parseSync();
|
|
102
|
-
|
|
103
|
-
saveLogsToFile(args.logFile);
|
|
104
|
-
}
|
|
105
|
-
function readPackageJson() {
|
|
106
|
-
const currentDir = import.meta.dirname;
|
|
107
|
-
const packageJsonPath = path.join(currentDir, '..', '..', 'package.json');
|
|
108
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
109
|
-
return {};
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
const json = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
113
|
-
assert.strict(json['name'], 'chrome-devtools-mcp');
|
|
114
|
-
return json;
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return {};
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
const version = readPackageJson().version ?? 'unknown';
|
|
118
|
+
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
|
|
121
119
|
logger(`Starting Chrome DevTools MCP Server v${version}`);
|
|
122
120
|
const server = new McpServer({
|
|
123
121
|
name: 'chrome_devtools',
|
|
@@ -136,6 +134,7 @@ async function getContext() {
|
|
|
136
134
|
customDevTools: args.customDevtools,
|
|
137
135
|
channel: args.channel,
|
|
138
136
|
isolated: args.isolated,
|
|
137
|
+
logFile,
|
|
139
138
|
});
|
|
140
139
|
if (context?.browser !== browser) {
|
|
141
140
|
context = await McpContext.from(browser, logger);
|
package/build/src/logger.js
CHANGED
package/build/src/tools/input.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import z from 'zod';
|
|
7
7
|
import { defineTool } from './ToolDefinition.js';
|
|
8
8
|
import { ToolCategories } from './categories.js';
|
|
9
|
-
import { waitForEventsAfterAction } from '../waitForHelpers.js';
|
|
10
9
|
export const click = defineTool({
|
|
11
10
|
name: 'click',
|
|
12
11
|
description: `Clicks on the provided element`,
|
|
@@ -27,7 +26,7 @@ export const click = defineTool({
|
|
|
27
26
|
const uid = request.params.uid;
|
|
28
27
|
const handle = await context.getElementByUid(uid);
|
|
29
28
|
try {
|
|
30
|
-
await waitForEventsAfterAction(
|
|
29
|
+
await context.waitForEventsAfterAction(async () => {
|
|
31
30
|
await handle.asLocator().click({
|
|
32
31
|
count: request.params.dblClick ? 2 : 1,
|
|
33
32
|
});
|
|
@@ -58,7 +57,7 @@ export const hover = defineTool({
|
|
|
58
57
|
const uid = request.params.uid;
|
|
59
58
|
const handle = await context.getElementByUid(uid);
|
|
60
59
|
try {
|
|
61
|
-
await waitForEventsAfterAction(
|
|
60
|
+
await context.waitForEventsAfterAction(async () => {
|
|
62
61
|
await handle.asLocator().hover();
|
|
63
62
|
});
|
|
64
63
|
response.appendResponseLine(`Successfully hovered over the element`);
|
|
@@ -85,7 +84,7 @@ export const fill = defineTool({
|
|
|
85
84
|
handler: async (request, response, context) => {
|
|
86
85
|
const handle = await context.getElementByUid(request.params.uid);
|
|
87
86
|
try {
|
|
88
|
-
await waitForEventsAfterAction(
|
|
87
|
+
await context.waitForEventsAfterAction(async () => {
|
|
89
88
|
await handle.asLocator().fill(request.params.value);
|
|
90
89
|
});
|
|
91
90
|
response.appendResponseLine(`Successfully filled out the element`);
|
|
@@ -111,7 +110,7 @@ export const drag = defineTool({
|
|
|
111
110
|
const fromHandle = await context.getElementByUid(request.params.from_uid);
|
|
112
111
|
const toHandle = await context.getElementByUid(request.params.to_uid);
|
|
113
112
|
try {
|
|
114
|
-
await waitForEventsAfterAction(
|
|
113
|
+
await context.waitForEventsAfterAction(async () => {
|
|
115
114
|
await fromHandle.drag(toHandle);
|
|
116
115
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
117
116
|
await toHandle.drop(fromHandle);
|
|
@@ -144,7 +143,7 @@ export const fillForm = defineTool({
|
|
|
144
143
|
for (const element of request.params.elements) {
|
|
145
144
|
const handle = await context.getElementByUid(element.uid);
|
|
146
145
|
try {
|
|
147
|
-
await waitForEventsAfterAction(
|
|
146
|
+
await context.waitForEventsAfterAction(async () => {
|
|
148
147
|
await handle.asLocator().fill(element.value);
|
|
149
148
|
});
|
|
150
149
|
}
|
package/build/src/tools/pages.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import z from 'zod';
|
|
7
7
|
import { defineTool } from './ToolDefinition.js';
|
|
8
8
|
import { ToolCategories } from './categories.js';
|
|
9
|
-
import { waitForEventsAfterAction } from '../waitForHelpers.js';
|
|
10
9
|
export const listPages = defineTool({
|
|
11
10
|
name: 'list_pages',
|
|
12
11
|
description: `Get a list of pages open in the browser.`,
|
|
@@ -69,7 +68,7 @@ export const newPage = defineTool({
|
|
|
69
68
|
},
|
|
70
69
|
handler: async (request, response, context) => {
|
|
71
70
|
const page = await context.newPage();
|
|
72
|
-
await waitForEventsAfterAction(
|
|
71
|
+
await context.waitForEventsAfterAction(async () => {
|
|
73
72
|
await page.goto(request.params.url);
|
|
74
73
|
});
|
|
75
74
|
response.setIncludePages(true);
|
|
@@ -87,7 +86,7 @@ export const navigatePage = defineTool({
|
|
|
87
86
|
},
|
|
88
87
|
handler: async (request, response, context) => {
|
|
89
88
|
const page = context.getSelectedPage();
|
|
90
|
-
await waitForEventsAfterAction(
|
|
89
|
+
await context.waitForEventsAfterAction(async () => {
|
|
91
90
|
await page.goto(request.params.url);
|
|
92
91
|
});
|
|
93
92
|
response.setIncludePages(true);
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import z from 'zod';
|
|
7
7
|
import { defineTool } from './ToolDefinition.js';
|
|
8
|
-
import { getInsightOutput, getTraceSummary, parseRawTraceBuffer, } from '../trace-processing/parse.js';
|
|
8
|
+
import { getInsightOutput, getTraceSummary, parseRawTraceBuffer, traceResultIsSuccess, } from '../trace-processing/parse.js';
|
|
9
9
|
import { logger } from '../logger.js';
|
|
10
10
|
import { ToolCategories } from './categories.js';
|
|
11
11
|
export const startTrace = defineTool({
|
|
12
12
|
name: 'performance_start_trace',
|
|
13
|
-
description: 'Starts a performance trace recording',
|
|
13
|
+
description: 'Starts a performance trace recording on the selected page.',
|
|
14
14
|
annotations: {
|
|
15
15
|
category: ToolCategories.PERFORMANCE,
|
|
16
16
|
readOnlyHint: true,
|
|
@@ -77,7 +77,7 @@ export const startTrace = defineTool({
|
|
|
77
77
|
});
|
|
78
78
|
export const stopTrace = defineTool({
|
|
79
79
|
name: 'performance_stop_trace',
|
|
80
|
-
description: 'Stops the active performance trace recording',
|
|
80
|
+
description: 'Stops the active performance trace recording on the selected page.',
|
|
81
81
|
annotations: {
|
|
82
82
|
category: ToolCategories.PERFORMANCE,
|
|
83
83
|
readOnlyHint: true,
|
|
@@ -93,7 +93,7 @@ export const stopTrace = defineTool({
|
|
|
93
93
|
});
|
|
94
94
|
export const analyzeInsight = defineTool({
|
|
95
95
|
name: 'performance_analyze_insight',
|
|
96
|
-
description: 'Provides more detailed information on a specific Performance Insight that was highlighed in the results of a trace recording',
|
|
96
|
+
description: 'Provides more detailed information on a specific Performance Insight that was highlighed in the results of a trace recording.',
|
|
97
97
|
annotations: {
|
|
98
98
|
category: ToolCategories.PERFORMANCE,
|
|
99
99
|
readOnlyHint: true,
|
|
@@ -122,20 +122,22 @@ async function stopTracingAndAppendOutput(page, response, context) {
|
|
|
122
122
|
const traceEventsBuffer = await page.tracing.stop();
|
|
123
123
|
const result = await parseRawTraceBuffer(traceEventsBuffer);
|
|
124
124
|
response.appendResponseLine('The performance trace has been stopped.');
|
|
125
|
-
if (result) {
|
|
125
|
+
if (traceResultIsSuccess(result)) {
|
|
126
126
|
context.storeTraceRecording(result);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
127
|
+
response.appendResponseLine('Here is a high level summary of the trace and the Insights that were found:');
|
|
128
|
+
const traceSummaryText = getTraceSummary(result);
|
|
129
|
+
response.appendResponseLine(traceSummaryText);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
response.appendResponseLine('There was an unexpected error parsing the trace:');
|
|
133
|
+
response.appendResponseLine(result.error);
|
|
135
134
|
}
|
|
136
135
|
}
|
|
137
136
|
catch (e) {
|
|
138
|
-
|
|
137
|
+
const errorText = e instanceof Error ? e.message : JSON.stringify(e);
|
|
138
|
+
logger(`Error stopping performance trace: ${errorText}`);
|
|
139
|
+
response.appendResponseLine('An error occured generating the response for this trace:');
|
|
140
|
+
response.appendResponseLine(errorText);
|
|
139
141
|
}
|
|
140
142
|
finally {
|
|
141
143
|
context.setIsRunningPerformanceTrace(false);
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import z from 'zod';
|
|
7
7
|
import { defineTool } from './ToolDefinition.js';
|
|
8
8
|
import { ToolCategories } from './categories.js';
|
|
9
|
-
import { waitForEventsAfterAction } from '../waitForHelpers.js';
|
|
10
9
|
export const evaluateScript = defineTool({
|
|
11
10
|
name: 'evaluate_script',
|
|
12
11
|
description: `Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON
|
|
@@ -43,7 +42,7 @@ Example with arguments: \`(el) => {
|
|
|
43
42
|
for (const el of request.params.args ?? []) {
|
|
44
43
|
args.push(await context.getElementByUid(el.uid));
|
|
45
44
|
}
|
|
46
|
-
await waitForEventsAfterAction(
|
|
45
|
+
await context.waitForEventsAfterAction(async () => {
|
|
47
46
|
const result = await page.evaluate(async (fn, ...args) => {
|
|
48
47
|
// @ts-expect-error no types.
|
|
49
48
|
return JSON.stringify(await fn(...args));
|
|
@@ -9,14 +9,21 @@ import * as TraceEngine from '../../node_modules/chrome-devtools-frontend/front_
|
|
|
9
9
|
import { logger } from '../logger.js';
|
|
10
10
|
import { AgentFocus } from '../../node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js';
|
|
11
11
|
const engine = TraceEngine.TraceModel.Model.createWithAllHandlers();
|
|
12
|
+
export function traceResultIsSuccess(x) {
|
|
13
|
+
return 'parsedTrace' in x;
|
|
14
|
+
}
|
|
12
15
|
export async function parseRawTraceBuffer(buffer) {
|
|
13
16
|
engine.resetProcessor();
|
|
14
17
|
if (!buffer) {
|
|
15
|
-
return
|
|
18
|
+
return {
|
|
19
|
+
error: 'No buffer was provided.',
|
|
20
|
+
};
|
|
16
21
|
}
|
|
17
22
|
const asString = new TextDecoder().decode(buffer);
|
|
18
23
|
if (!asString) {
|
|
19
|
-
return
|
|
24
|
+
return {
|
|
25
|
+
error: 'Decoding the trace buffer returned an empty string.',
|
|
26
|
+
};
|
|
20
27
|
}
|
|
21
28
|
try {
|
|
22
29
|
const data = JSON.parse(asString);
|
|
@@ -24,25 +31,22 @@ export async function parseRawTraceBuffer(buffer) {
|
|
|
24
31
|
await engine.parse(events);
|
|
25
32
|
const parsedTrace = engine.parsedTrace();
|
|
26
33
|
if (!parsedTrace) {
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!insights) {
|
|
31
|
-
return null;
|
|
34
|
+
return {
|
|
35
|
+
error: 'No parsed trace was returned from the trace engine.',
|
|
36
|
+
};
|
|
32
37
|
}
|
|
38
|
+
const insights = parsedTrace?.insights ?? null;
|
|
33
39
|
return {
|
|
34
40
|
parsedTrace,
|
|
35
41
|
insights,
|
|
36
42
|
};
|
|
37
43
|
}
|
|
38
44
|
catch (e) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
return null;
|
|
45
|
+
const errorText = e instanceof Error ? e.message : JSON.stringify(e);
|
|
46
|
+
logger(`Unexpeced error parsing trace: ${errorText}`);
|
|
47
|
+
return {
|
|
48
|
+
error: errorText,
|
|
49
|
+
};
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
export function getTraceSummary(result) {
|
|
@@ -53,6 +57,11 @@ export function getTraceSummary(result) {
|
|
|
53
57
|
return output;
|
|
54
58
|
}
|
|
55
59
|
export function getInsightOutput(result, insightName) {
|
|
60
|
+
if (!result.insights) {
|
|
61
|
+
return {
|
|
62
|
+
error: 'No Performance insights are available for this trace.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
56
65
|
// Currently, we do not support inspecting traces with multiple navigations. We either:
|
|
57
66
|
// 1. Find Insights from the first navigation (common case: user records a trace with a page reload to test load performance)
|
|
58
67
|
// 2. Fall back to finding Insights not associated with a navigation (common case: user tests an interaction without a page load).
|