chrome-devtools-mcp 0.2.0 → 0.2.1
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 +123 -0
- package/build/src/browser.js +12 -9
- package/build/src/index.js +3 -5
- 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 +3 -3
- package/build/src/tools/script.js +1 -2
- package/package.json +12 -11
- 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,123 @@
|
|
|
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 navigationStartedPromise = this.waitForNavigationStarted();
|
|
103
|
+
await action();
|
|
104
|
+
try {
|
|
105
|
+
const navigationStated = await navigationStartedPromise;
|
|
106
|
+
if (navigationStated) {
|
|
107
|
+
await this.#page.waitForNavigation({
|
|
108
|
+
timeout: this.#navigationTimeout,
|
|
109
|
+
signal: this.#abortController.signal,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Wait for stable dom after navigation so we execute in
|
|
113
|
+
// the correct context
|
|
114
|
+
await this.waitForStableDom();
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
logger(error);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
this.#abortController.abort();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
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',
|
|
@@ -99,9 +98,7 @@ export const args = yargsInstance
|
|
|
99
98
|
.wrap(Math.min(120, yargsInstance.terminalWidth()))
|
|
100
99
|
.help()
|
|
101
100
|
.parseSync();
|
|
102
|
-
|
|
103
|
-
saveLogsToFile(args.logFile);
|
|
104
|
-
}
|
|
101
|
+
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
|
|
105
102
|
function readPackageJson() {
|
|
106
103
|
const currentDir = import.meta.dirname;
|
|
107
104
|
const packageJsonPath = path.join(currentDir, '..', '..', 'package.json');
|
|
@@ -136,6 +133,7 @@ async function getContext() {
|
|
|
136
133
|
customDevTools: args.customDevtools,
|
|
137
134
|
channel: args.channel,
|
|
138
135
|
isolated: args.isolated,
|
|
136
|
+
logFile,
|
|
139
137
|
});
|
|
140
138
|
if (context?.browser !== browser) {
|
|
141
139
|
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);
|
|
@@ -10,7 +10,7 @@ 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,
|
|
@@ -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));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "MCP server for Chrome DevTools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,17 +8,18 @@
|
|
|
8
8
|
},
|
|
9
9
|
"main": "index.js",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsc && node --experimental-strip-types scripts/post-build.ts",
|
|
11
|
+
"build": "tsc && node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/post-build.ts",
|
|
12
12
|
"typecheck": "tsc --noEmit",
|
|
13
|
-
"format": "eslint --cache --fix .
|
|
14
|
-
"check-format": "eslint --cache
|
|
15
|
-
"
|
|
13
|
+
"format": "eslint --cache --fix . && prettier --write --cache .",
|
|
14
|
+
"check-format": "eslint --cache . && prettier --check --cache .;",
|
|
15
|
+
"docs": "npm run build && npm run docs:generate && npm run format",
|
|
16
|
+
"docs:generate": "node --experimental-strip-types scripts/generate-docs.ts",
|
|
16
17
|
"start": "npm run build && node build/src/index.js",
|
|
17
18
|
"start-debug": "DEBUG=mcp:* DEBUG_COLORS=false npm run build && node build/src/index.js",
|
|
18
|
-
"test": "npm run build && node --require ./build/tests/setup.js --test-reporter spec --test-force-exit --test
|
|
19
|
-
"test:only": "npm run build && node --require ./build/tests/setup.js --test-reporter spec --test-force-exit --test --test-only
|
|
20
|
-
"test:only:no-build": "node --require ./build/tests/setup.js --test-reporter spec --test-force-exit --test --test-only
|
|
21
|
-
"test:update-snapshots": "npm run build && node --require ./build/tests/setup.js --test-force-exit --test --test-update-snapshots
|
|
19
|
+
"test": "npm run build && node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test \"build/tests/**/*.test.js\"",
|
|
20
|
+
"test:only": "npm run build && node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test --test-only \"build/tests/**/*.test.js\"",
|
|
21
|
+
"test:only:no-build": "node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test --test-only \"build/tests/**/*.test.js\"",
|
|
22
|
+
"test:update-snapshots": "npm run build && node --require ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-force-exit --test --test-update-snapshots \"build/tests/**/*.test.js\"",
|
|
22
23
|
"prepare": "node --experimental-strip-types scripts/prepare.ts"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
},
|
|
36
37
|
"homepage": "https://github.com/ChromeDevTools/chrome-devtools-mcp#readme",
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@modelcontextprotocol/sdk": "1.18.
|
|
39
|
+
"@modelcontextprotocol/sdk": "1.18.1",
|
|
39
40
|
"debug": "4.4.3",
|
|
40
41
|
"puppeteer-core": "24.22.0",
|
|
41
42
|
"yargs": "18.0.0"
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"@types/yargs": "^17.0.33",
|
|
50
51
|
"@typescript-eslint/eslint-plugin": "^8.43.0",
|
|
51
52
|
"@typescript-eslint/parser": "^8.43.0",
|
|
52
|
-
"chrome-devtools-frontend": "1.0.
|
|
53
|
+
"chrome-devtools-frontend": "1.0.1516909",
|
|
53
54
|
"eslint": "^9.35.0",
|
|
54
55
|
"globals": "^16.4.0",
|
|
55
56
|
"prettier": "^3.6.2",
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { logger } from './logger.js';
|
|
2
|
-
async function waitForStableDom(page, signal) {
|
|
3
|
-
const stableDomObserver = await page.evaluateHandle(() => {
|
|
4
|
-
let timeoutId;
|
|
5
|
-
function callback() {
|
|
6
|
-
clearTimeout(timeoutId);
|
|
7
|
-
timeoutId = setTimeout(() => {
|
|
8
|
-
domObserver.resolver.resolve();
|
|
9
|
-
domObserver.observer.disconnect();
|
|
10
|
-
}, 100);
|
|
11
|
-
}
|
|
12
|
-
const domObserver = {
|
|
13
|
-
resolver: Promise.withResolvers(),
|
|
14
|
-
observer: new MutationObserver(callback),
|
|
15
|
-
};
|
|
16
|
-
// It's possible that the DOM is not gonna change so we
|
|
17
|
-
// need to start the timeout initially.
|
|
18
|
-
callback();
|
|
19
|
-
domObserver.observer.observe(document.body, {
|
|
20
|
-
childList: true,
|
|
21
|
-
subtree: true,
|
|
22
|
-
attributes: true,
|
|
23
|
-
});
|
|
24
|
-
return domObserver;
|
|
25
|
-
});
|
|
26
|
-
signal.addEventListener('abort', async () => {
|
|
27
|
-
try {
|
|
28
|
-
await stableDomObserver.evaluate(observer => {
|
|
29
|
-
observer.observer.disconnect();
|
|
30
|
-
observer.resolver.resolve();
|
|
31
|
-
});
|
|
32
|
-
await stableDomObserver.dispose();
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
// Ignored cleanup errors
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
return Promise.race([
|
|
39
|
-
stableDomObserver.evaluate(async (observer) => {
|
|
40
|
-
return await observer.resolver.promise;
|
|
41
|
-
}),
|
|
42
|
-
timeout(3000, signal).then(() => {
|
|
43
|
-
throw new Error('Timeout');
|
|
44
|
-
}),
|
|
45
|
-
]);
|
|
46
|
-
}
|
|
47
|
-
async function waitForNavigationStarted(page, signal) {
|
|
48
|
-
// Currently Puppeteer does not have API
|
|
49
|
-
// For when a navigation is about to start
|
|
50
|
-
const navigationStartedPromise = new Promise(resolve => {
|
|
51
|
-
const listener = (event) => {
|
|
52
|
-
if ([
|
|
53
|
-
'historySameDocument',
|
|
54
|
-
'historyDifferentDocument',
|
|
55
|
-
'sameDocument',
|
|
56
|
-
].includes(event.navigationType)) {
|
|
57
|
-
resolve(false);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
resolve(true);
|
|
61
|
-
};
|
|
62
|
-
page._client().on('Page.frameStartedNavigating', listener);
|
|
63
|
-
signal.addEventListener('abort', () => {
|
|
64
|
-
resolve(false);
|
|
65
|
-
page._client().off('Page.frameStartedNavigating', listener);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
return await Promise.race([
|
|
69
|
-
navigationStartedPromise,
|
|
70
|
-
timeout(100).then(() => false),
|
|
71
|
-
]);
|
|
72
|
-
}
|
|
73
|
-
function timeout(time, signal) {
|
|
74
|
-
return new Promise(res => {
|
|
75
|
-
const id = setTimeout(res, time);
|
|
76
|
-
signal?.addEventListener('abort', () => {
|
|
77
|
-
res();
|
|
78
|
-
clearTimeout(id);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* A wrapper that executes a action and waits for
|
|
84
|
-
* a potential navigation, after which it waits
|
|
85
|
-
* for the DOM to be stable before returning.
|
|
86
|
-
*/
|
|
87
|
-
export async function waitForEventsAfterAction(page, callback) {
|
|
88
|
-
const controller = new AbortController();
|
|
89
|
-
const navigationStartedPromise = waitForNavigationStarted(page, controller.signal);
|
|
90
|
-
await callback();
|
|
91
|
-
try {
|
|
92
|
-
const navigationStated = await navigationStartedPromise;
|
|
93
|
-
if (navigationStated) {
|
|
94
|
-
await page.waitForNavigation({
|
|
95
|
-
timeout: 3000,
|
|
96
|
-
signal: controller.signal,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
// Wait for stable dom after navigation so we execute in
|
|
100
|
-
// the correct context
|
|
101
|
-
await waitForStableDom(page, controller.signal);
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
logger(error);
|
|
105
|
-
}
|
|
106
|
-
finally {
|
|
107
|
-
controller.abort();
|
|
108
|
-
}
|
|
109
|
-
}
|