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.
Files changed (61) hide show
  1. package/README.md +27 -8
  2. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Progress.js +60 -53
  3. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/GdpClient.js +1 -1
  4. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +5 -2
  5. package/build/node_modules/chrome-devtools-frontend/front_end/core/protocol_client/InspectorBackend.js +2 -0
  6. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +11 -10
  7. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSModel.js +1 -1
  8. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSPropertyParserMatchers.js +24 -4
  9. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/DebuggerModel.js +1 -1
  10. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/EnhancedTracesParser.js +29 -24
  11. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +1 -1
  12. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +1 -1
  13. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js +9 -15
  14. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RemoteObject.js +1 -1
  15. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ResourceTreeModel.js +1 -1
  16. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RuntimeModel.js +1 -1
  17. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ServiceWorkerManager.js +1 -1
  18. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMap.js +4 -31
  19. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/TraceObject.js +5 -2
  20. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.js +6 -4
  21. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +259 -179
  22. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/UnitFormatters.js +10 -1
  23. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js +14 -3
  24. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ContentProviderBasedProject.js +6 -4
  25. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +2 -2
  26. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/ModelImpl.js +4 -9
  27. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/Processor.js +17 -9
  28. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/AuctionWorkletsHandler.js +1 -1
  29. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/FramesHandler.js +2 -2
  30. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/LayoutShiftsHandler.js +3 -4
  31. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/MetaHandler.js +10 -9
  32. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/ScreenshotsHandler.js +0 -1
  33. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/ScriptsHandler.js +4 -4
  34. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserInteractionsHandler.js +2 -10
  35. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserTimingsHandler.js +3 -4
  36. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/SamplesIntegrator.js +8 -6
  37. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/CLSCulprits.js +1 -1
  38. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DocumentLatency.js +3 -3
  39. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DuplicatedJavaScript.js +1 -1
  40. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/INPBreakdown.js +1 -1
  41. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/ImageDelivery.js +1 -1
  42. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LCPBreakdown.js +1 -1
  43. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LCPDiscovery.js +1 -1
  44. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/ModernHTTP.js +1 -1
  45. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/NetworkDependencyTree.js +1 -1
  46. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/RenderBlocking.js +1 -1
  47. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js +21 -21
  48. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/SourceMapsResolver.js +5 -3
  49. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/trace_source_maps_resolver.js +1 -1
  50. package/build/src/McpContext.js +60 -10
  51. package/build/src/McpResponse.js +5 -4
  52. package/build/src/WaitForHelper.js +123 -0
  53. package/build/src/browser.js +12 -9
  54. package/build/src/index.js +3 -5
  55. package/build/src/logger.js +1 -0
  56. package/build/src/tools/input.js +5 -6
  57. package/build/src/tools/pages.js +2 -3
  58. package/build/src/tools/performance.js +3 -3
  59. package/build/src/tools/script.js +1 -2
  60. package/package.json +12 -11
  61. package/build/src/waitForHelpers.js +0 -109
@@ -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
- #networkConditions = null;
18
- #cpuThrottlingRate = 1;
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', (event) => {
48
+ page.on('console', event => {
32
49
  collect(event);
33
50
  });
34
- page.on('pageerror', (event) => {
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
- this.#networkConditions = conditions;
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
- return this.#networkConditions;
106
+ const page = this.getSelectedPage();
107
+ return this.#networkConditionsMap.get(page) ?? null;
83
108
  }
84
109
  setCpuThrottlingRate(rate) {
85
- this.#cpuThrottlingRate = rate;
110
+ const page = this.getSelectedPage();
111
+ this.#cpuThrottlingRateMap.set(page, rate);
112
+ this.#updateSelectedPageTimeouts();
86
113
  }
87
114
  getCpuThrottlingRate() {
88
- return this.#cpuThrottlingRate;
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
- newPage.setDefaultTimeout(5_000);
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
- newPage.setDefaultNavigationTimeout(10_000);
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
  }
@@ -8,7 +8,7 @@ export class McpResponse {
8
8
  #attachedNetworkRequestUrl;
9
9
  #includeConsoleData = false;
10
10
  #textResponseLines = [];
11
- #formatedConsoleData;
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.#formatedConsoleData = formattedConsoleMessages;
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.#formatedConsoleData) {
124
+ if (this.#includeConsoleData && this.#formattedConsoleData) {
124
125
  response.push('## Console messages');
125
- response.push(...this.#formatedConsoleData);
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
+ }
@@ -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
- return await puppeteer.launch({
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
  });
@@ -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
- if (args.logFile) {
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);
@@ -22,5 +22,6 @@ export function saveLogsToFile(fileName) {
22
22
  logFile.end();
23
23
  process.exit(1);
24
24
  });
25
+ return logFile;
25
26
  }
26
27
  export const logger = debug(mcpDebugNamespace);
@@ -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(handle.frame.page(), async () => {
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(handle.frame.page(), async () => {
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(handle.frame.page(), async () => {
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(fromHandle.frame.page(), async () => {
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(handle.frame.page(), async () => {
146
+ await context.waitForEventsAfterAction(async () => {
148
147
  await handle.asLocator().fill(element.value);
149
148
  });
150
149
  }
@@ -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(page, async () => {
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(page, async () => {
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(page, async () => {
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.0",
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 . ;prettier --write --cache .",
14
- "check-format": "eslint --cache .; prettier --check --cache .;",
15
- "generate-docs": "npm run build && node --experimental-strip-types scripts/generate-docs.ts",
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 'build/tests/**/*.test.js'",
19
- "test:only": "npm run build && node --require ./build/tests/setup.js --test-reporter spec --test-force-exit --test --test-only 'build/tests/**/*.test.js'",
20
- "test:only:no-build": "node --require ./build/tests/setup.js --test-reporter spec --test-force-exit --test --test-only 'build/tests/**/*.test.js'",
21
- "test:update-snapshots": "npm run build && node --require ./build/tests/setup.js --test-force-exit --test --test-update-snapshots 'build/tests/**/*.test.js'",
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.0",
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.1515796",
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
- }