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.
Files changed (62) 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 +127 -0
  53. package/build/src/browser.js +12 -9
  54. package/build/src/index.js +20 -21
  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 +16 -14
  59. package/build/src/tools/script.js +1 -2
  60. package/build/src/trace-processing/parse.js +23 -14
  61. package/package.json +15 -16
  62. 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,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
+ }
@@ -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',
@@ -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
- if (args.logFile) {
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);
@@ -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);
@@ -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
- const insightText = getTraceSummary(result);
128
- if (insightText) {
129
- response.appendResponseLine('Insights with performance opportunities:');
130
- response.appendResponseLine(insightText);
131
- }
132
- else {
133
- response.appendResponseLine('No insights have been found. The performance looks good!');
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
- logger(`Error stopping performance trace: ${e instanceof Error ? e.message : JSON.stringify(e)}`);
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(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));
@@ -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 null;
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 null;
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 null;
28
- }
29
- const insights = parsedTrace?.insights;
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
- if (e instanceof Error) {
40
- logger(`Error parsing trace: ${e.message}`);
41
- }
42
- else {
43
- logger(`Error parsing trace: ${JSON.stringify(e)}`);
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).