chrome-devtools-mcp 0.0.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +6 -3
  2. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Settings.js +3 -32
  3. package/build/node_modules/chrome-devtools-frontend/front_end/core/i18n/i18n.js +35 -8
  4. package/build/node_modules/chrome-devtools-frontend/front_end/core/root/Runtime.js +4 -1
  5. package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +4 -4
  6. package/build/node_modules/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +12 -0
  7. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +366 -0
  8. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +366 -0
  9. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js +64 -0
  10. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIQueries.js +105 -0
  11. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CSSWorkspaceBinding.js +243 -0
  12. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CompilerScriptMapping.js +407 -0
  13. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ContentProviderBasedProject.js +128 -0
  14. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DebuggerLanguagePlugins.js +992 -0
  15. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DebuggerWorkspaceBinding.js +574 -0
  16. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DefaultScriptMapping.js +112 -0
  17. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/FileUtils.js +186 -0
  18. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/LiveLocation.js +60 -0
  19. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/NetworkProject.js +107 -0
  20. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/PresentationConsoleMessageHelper.js +244 -0
  21. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceMapping.js +473 -0
  22. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceScriptMapping.js +399 -0
  23. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceUtils.js +87 -0
  24. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/SASSSourceMapping.js +181 -0
  25. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/StylesSourceMapping.js +268 -0
  26. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/TempFile.js +55 -0
  27. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/bindings.js +20 -0
  28. package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/CrUXManager.js +283 -0
  29. package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/crux-manager.js +4 -0
  30. package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/DeviceModeModel.js +775 -0
  31. package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/EmulatedDevices.js +1706 -0
  32. package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/emulation.js +6 -0
  33. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +131 -0
  34. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/ScriptFormatter.js +77 -0
  35. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/formatter.js +6 -0
  36. package/build/node_modules/chrome-devtools-frontend/front_end/models/geometry/GeometryImpl.js +347 -0
  37. package/build/node_modules/chrome-devtools-frontend/front_end/models/geometry/geometry.js +4 -0
  38. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/NamesResolver.js +626 -0
  39. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/ScopeChainModel.js +59 -0
  40. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/ScopeTreeCache.js +32 -0
  41. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/source_map_scopes.js +7 -0
  42. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTrace.js +4 -0
  43. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceImpl.js +67 -0
  44. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceModel.js +97 -0
  45. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/Trie.js +113 -0
  46. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/stack_trace.js +5 -0
  47. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/stack_trace_impl.js +7 -0
  48. package/build/node_modules/chrome-devtools-frontend/front_end/models/text_utils/TextUtils.js +23 -0
  49. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/Processor.js +1 -1
  50. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Trace.js +1 -1
  51. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DocumentLatency.js +5 -4
  52. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/SourceMapsResolver.js +199 -0
  53. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/trace_source_maps_resolver.js +4 -0
  54. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/FileManager.js +64 -0
  55. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/IgnoreListManager.js +511 -0
  56. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/SearchConfig.js +113 -0
  57. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/UISourceCode.js +563 -0
  58. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/WorkspaceImpl.js +204 -0
  59. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/workspace.js +9 -0
  60. package/build/src/McpContext.js +24 -9
  61. package/build/src/McpResponse.js +3 -3
  62. package/build/src/browser.js +3 -1
  63. package/build/src/index.js +1 -1
  64. package/build/src/tools/input.js +7 -7
  65. package/build/src/tools/performance.js +29 -2
  66. package/build/src/tools/screenshot.js +1 -1
  67. package/build/src/tools/script.js +40 -14
  68. package/build/src/trace-processing/parse.js +26 -22
  69. package/package.json +9 -7
@@ -0,0 +1,204 @@
1
+ // Copyright 2012 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+ import * as Common from '../../core/common/common.js';
5
+ import { UISourceCode } from './UISourceCode.js';
6
+ /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */
7
+ export var projectTypes;
8
+ (function (projectTypes) {
9
+ projectTypes["Debugger"] = "debugger";
10
+ projectTypes["Formatter"] = "formatter";
11
+ projectTypes["Network"] = "network";
12
+ projectTypes["FileSystem"] = "filesystem";
13
+ projectTypes["ConnectableFileSystem"] = "connectablefilesystem";
14
+ projectTypes["ContentScripts"] = "contentscripts";
15
+ projectTypes["Service"] = "service";
16
+ })(projectTypes || (projectTypes = {}));
17
+ /* eslint-enable @typescript-eslint/naming-convention */
18
+ export class ProjectStore {
19
+ #workspace;
20
+ #id;
21
+ #type;
22
+ #displayName;
23
+ #uiSourceCodes = new Map();
24
+ constructor(workspace, id, type, displayName) {
25
+ this.#workspace = workspace;
26
+ this.#id = id;
27
+ this.#type = type;
28
+ this.#displayName = displayName;
29
+ }
30
+ id() {
31
+ return this.#id;
32
+ }
33
+ type() {
34
+ return this.#type;
35
+ }
36
+ displayName() {
37
+ return this.#displayName;
38
+ }
39
+ workspace() {
40
+ return this.#workspace;
41
+ }
42
+ createUISourceCode(url, contentType) {
43
+ return new UISourceCode(this, url, contentType);
44
+ }
45
+ addUISourceCode(uiSourceCode) {
46
+ const url = uiSourceCode.url();
47
+ if (this.uiSourceCodeForURL(url)) {
48
+ return false;
49
+ }
50
+ this.#uiSourceCodes.set(url, uiSourceCode);
51
+ this.#workspace.dispatchEventToListeners(Events.UISourceCodeAdded, uiSourceCode);
52
+ return true;
53
+ }
54
+ removeUISourceCode(url) {
55
+ const uiSourceCode = this.#uiSourceCodes.get(url);
56
+ if (uiSourceCode === undefined) {
57
+ return;
58
+ }
59
+ this.#uiSourceCodes.delete(url);
60
+ this.#workspace.dispatchEventToListeners(Events.UISourceCodeRemoved, uiSourceCode);
61
+ }
62
+ removeProject() {
63
+ this.#workspace.removeProject(this);
64
+ this.#uiSourceCodes.clear();
65
+ }
66
+ uiSourceCodeForURL(url) {
67
+ return this.#uiSourceCodes.get(url) ?? null;
68
+ }
69
+ uiSourceCodes() {
70
+ return this.#uiSourceCodes.values();
71
+ }
72
+ renameUISourceCode(uiSourceCode, newName) {
73
+ const oldPath = uiSourceCode.url();
74
+ const newPath = uiSourceCode.parentURL() ?
75
+ Common.ParsedURL.ParsedURL.urlFromParentUrlAndName(uiSourceCode.parentURL(), newName) :
76
+ Common.ParsedURL.ParsedURL.preEncodeSpecialCharactersInPath(newName);
77
+ this.#uiSourceCodes.set(newPath, uiSourceCode);
78
+ this.#uiSourceCodes.delete(oldPath);
79
+ }
80
+ // No-op implementation for a handful of interface methods.
81
+ rename(_uiSourceCode, _newName, _callback) {
82
+ }
83
+ excludeFolder(_path) {
84
+ }
85
+ deleteFile(_uiSourceCode) {
86
+ }
87
+ deleteDirectoryRecursively(_path) {
88
+ return Promise.resolve(false);
89
+ }
90
+ remove() {
91
+ }
92
+ indexContent(_progress) {
93
+ }
94
+ }
95
+ let workspaceInstance;
96
+ export class WorkspaceImpl extends Common.ObjectWrapper.ObjectWrapper {
97
+ #projects = new Map();
98
+ #hasResourceContentTrackingExtensions = false;
99
+ constructor() {
100
+ super();
101
+ }
102
+ static instance(opts = { forceNew: null }) {
103
+ const { forceNew } = opts;
104
+ if (!workspaceInstance || forceNew) {
105
+ workspaceInstance = new WorkspaceImpl();
106
+ }
107
+ return workspaceInstance;
108
+ }
109
+ static removeInstance() {
110
+ workspaceInstance = undefined;
111
+ }
112
+ uiSourceCode(projectId, url) {
113
+ const project = this.#projects.get(projectId);
114
+ return project ? project.uiSourceCodeForURL(url) : null;
115
+ }
116
+ uiSourceCodeForURL(url) {
117
+ for (const project of this.#projects.values()) {
118
+ const uiSourceCode = project.uiSourceCodeForURL(url);
119
+ if (uiSourceCode) {
120
+ return uiSourceCode;
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+ findCompatibleUISourceCodes(uiSourceCode) {
126
+ const url = uiSourceCode.url();
127
+ const contentType = uiSourceCode.contentType();
128
+ const result = [];
129
+ for (const project of this.#projects.values()) {
130
+ if (uiSourceCode.project().type() !== project.type()) {
131
+ continue;
132
+ }
133
+ const candidate = project.uiSourceCodeForURL(url);
134
+ if (candidate && candidate.url() === url && candidate.contentType() === contentType) {
135
+ result.push(candidate);
136
+ }
137
+ }
138
+ return result;
139
+ }
140
+ uiSourceCodesForProjectType(type) {
141
+ const result = [];
142
+ for (const project of this.#projects.values()) {
143
+ if (project.type() === type) {
144
+ for (const uiSourceCode of project.uiSourceCodes()) {
145
+ result.push(uiSourceCode);
146
+ }
147
+ }
148
+ }
149
+ return result;
150
+ }
151
+ addProject(project) {
152
+ console.assert(!this.#projects.has(project.id()), `A project with id ${project.id()} already exists!`);
153
+ this.#projects.set(project.id(), project);
154
+ this.dispatchEventToListeners(Events.ProjectAdded, project);
155
+ }
156
+ removeProject(project) {
157
+ this.#projects.delete(project.id());
158
+ this.dispatchEventToListeners(Events.ProjectRemoved, project);
159
+ }
160
+ project(projectId) {
161
+ return this.#projects.get(projectId) || null;
162
+ }
163
+ projectForFileSystemRoot(root) {
164
+ const projectId = Common.ParsedURL.ParsedURL.rawPathToUrlString(root);
165
+ return this.project(projectId);
166
+ }
167
+ projects() {
168
+ return [...this.#projects.values()];
169
+ }
170
+ projectsForType(type) {
171
+ function filterByType(project) {
172
+ return project.type() === type;
173
+ }
174
+ return this.projects().filter(filterByType);
175
+ }
176
+ uiSourceCodes() {
177
+ const result = [];
178
+ for (const project of this.#projects.values()) {
179
+ for (const uiSourceCode of project.uiSourceCodes()) {
180
+ result.push(uiSourceCode);
181
+ }
182
+ }
183
+ return result;
184
+ }
185
+ setHasResourceContentTrackingExtensions(hasExtensions) {
186
+ this.#hasResourceContentTrackingExtensions = hasExtensions;
187
+ }
188
+ hasResourceContentTrackingExtensions() {
189
+ return this.#hasResourceContentTrackingExtensions;
190
+ }
191
+ }
192
+ export var Events;
193
+ (function (Events) {
194
+ /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */
195
+ Events["UISourceCodeAdded"] = "UISourceCodeAdded";
196
+ Events["UISourceCodeRemoved"] = "UISourceCodeRemoved";
197
+ Events["UISourceCodeRenamed"] = "UISourceCodeRenamed";
198
+ Events["WorkingCopyChanged"] = "WorkingCopyChanged";
199
+ Events["WorkingCopyCommitted"] = "WorkingCopyCommitted";
200
+ Events["WorkingCopyCommittedByUser"] = "WorkingCopyCommittedByUser";
201
+ Events["ProjectAdded"] = "ProjectAdded";
202
+ Events["ProjectRemoved"] = "ProjectRemoved";
203
+ /* eslint-enable @typescript-eslint/naming-convention */
204
+ })(Events || (Events = {}));
@@ -0,0 +1,9 @@
1
+ // Copyright 2019 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+ import * as FileManager from './FileManager.js';
5
+ import * as IgnoreListManager from './IgnoreListManager.js';
6
+ import * as SearchConfig from './SearchConfig.js';
7
+ import * as UISourceCode from './UISourceCode.js';
8
+ import * as Workspace from './WorkspaceImpl.js';
9
+ export { FileManager, IgnoreListManager, SearchConfig, UISourceCode, Workspace, };
@@ -11,13 +11,14 @@ export class McpContext {
11
11
  #selectedPageIdx = 0;
12
12
  // The most recent snapshot.
13
13
  #textSnapshot = null;
14
- #idToNodeMap = new Map();
15
14
  #networkCollector;
16
15
  #consoleCollector;
17
16
  #isRunningTrace = false;
18
17
  #networkConditions = null;
19
18
  #cpuThrottlingRate = 1;
20
19
  #dialog;
20
+ #nextSnapshotId = 1;
21
+ #traceResults = [];
21
22
  constructor(browser, logger) {
22
23
  this.browser = browser;
23
24
  this.logger = logger;
@@ -135,10 +136,14 @@ export class McpContext {
135
136
  newPage.setDefaultNavigationTimeout(10_000);
136
137
  }
137
138
  async getElementByUid(uid) {
138
- if (!this.#idToNodeMap.size) {
139
+ if (!this.#textSnapshot?.idToNode.size) {
139
140
  throw new Error('No snapshot found. Use browser_snapshot to capture one');
140
141
  }
141
- const node = this.#idToNodeMap.get(uid);
142
+ const [snapshotId] = uid.split('_');
143
+ if (this.#textSnapshot.snapshotId !== snapshotId) {
144
+ throw new Error('This uid is coming from a stale snapshot. Call take_snapshot to get a fresh snapshot.');
145
+ }
146
+ const node = this.#textSnapshot?.idToNode.get(uid);
142
147
  if (!node) {
143
148
  throw new Error('No such element found in the snapshot');
144
149
  }
@@ -165,26 +170,30 @@ export class McpContext {
165
170
  const page = this.getSelectedPage();
166
171
  const rootNode = await page.accessibility.snapshot();
167
172
  if (!rootNode) {
168
- return null;
173
+ return;
169
174
  }
175
+ const snapshotId = this.#nextSnapshotId++;
170
176
  // Iterate through the whole accessibility node tree and assign node ids that
171
177
  // will be used for the tree serialization and mapping ids back to nodes.
172
178
  let idCounter = 0;
173
- this.#idToNodeMap.clear();
179
+ const idToNode = new Map();
174
180
  const assignIds = (node) => {
175
181
  const nodeWithId = {
176
182
  ...node,
177
- id: idCounter++,
183
+ id: `${snapshotId}_${idCounter++}`,
178
184
  children: node.children
179
185
  ? node.children.map(child => assignIds(child))
180
186
  : [],
181
187
  };
182
- this.#idToNodeMap.set(nodeWithId.id, nodeWithId);
188
+ idToNode.set(nodeWithId.id, nodeWithId);
183
189
  return nodeWithId;
184
190
  };
185
191
  const rootNodeWithId = assignIds(rootNode);
186
- this.#textSnapshot = rootNodeWithId;
187
- return rootNodeWithId;
192
+ this.#textSnapshot = {
193
+ root: rootNodeWithId,
194
+ snapshotId: String(snapshotId),
195
+ idToNode,
196
+ };
188
197
  }
189
198
  getTextSnapshot() {
190
199
  return this.#textSnapshot;
@@ -201,4 +210,10 @@ export class McpContext {
201
210
  throw new Error('Could not save a screenshot to a file');
202
211
  }
203
212
  }
213
+ storeTraceRecording(result) {
214
+ this.#traceResults.push(result);
215
+ }
216
+ recordedTraces() {
217
+ return this.#traceResults;
218
+ }
204
219
  }
@@ -100,9 +100,9 @@ Call browser_handle_dialog to handle it before continuing.`);
100
100
  response.push(...parts);
101
101
  }
102
102
  if (this.#includeSnapshot) {
103
- const rootNode = context.getTextSnapshot();
104
- if (rootNode) {
105
- const formattedSnapshot = formatA11ySnapshot(rootNode);
103
+ const snapshot = context.getTextSnapshot();
104
+ if (snapshot) {
105
+ const formattedSnapshot = formatA11ySnapshot(snapshot.root);
106
106
  response.push('## Page content');
107
107
  response.push(formattedSnapshot);
108
108
  }
@@ -43,7 +43,9 @@ async function ensureBrowserConnected(browserURL) {
43
43
  }
44
44
  export async function launch(options) {
45
45
  const { channel, executablePath, customDevTools, headless, isolated } = options;
46
- const profileDirName = channel && channel !== 'stable' ? `mcp-profile-${channel}` : 'mcp-profile';
46
+ const profileDirName = channel && channel !== 'stable'
47
+ ? `chrome-profile-${channel}`
48
+ : 'chrome-profile';
47
49
  let userDataDir = options.userDataDir;
48
50
  if (!isolated && !userDataDir) {
49
51
  userDataDir = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp', profileDirName);
@@ -137,7 +137,7 @@ async function getContext() {
137
137
  channel: args.channel,
138
138
  isolated: args.isolated,
139
139
  });
140
- if (!context) {
140
+ if (context?.browser !== browser) {
141
141
  context = await McpContext.from(browser, logger);
142
142
  }
143
143
  return context;
@@ -16,7 +16,7 @@ export const click = defineTool({
16
16
  },
17
17
  schema: {
18
18
  uid: z
19
- .number()
19
+ .string()
20
20
  .describe('The uid of an element on the page from the page content snapshot'),
21
21
  dblClick: z
22
22
  .boolean()
@@ -51,7 +51,7 @@ export const hover = defineTool({
51
51
  },
52
52
  schema: {
53
53
  uid: z
54
- .number()
54
+ .string()
55
55
  .describe('The uid of an element on the page from the page content snapshot'),
56
56
  },
57
57
  handler: async (request, response, context) => {
@@ -78,7 +78,7 @@ export const fill = defineTool({
78
78
  },
79
79
  schema: {
80
80
  uid: z
81
- .number()
81
+ .string()
82
82
  .describe('The uid of an element on the page from the page content snapshot'),
83
83
  value: z.string().describe('The value to fill in'),
84
84
  },
@@ -104,8 +104,8 @@ export const drag = defineTool({
104
104
  readOnlyHint: false,
105
105
  },
106
106
  schema: {
107
- from_uid: z.number().describe('The uid of the element to drag'),
108
- to_uid: z.number().describe('The uid of the element to drop into'),
107
+ from_uid: z.string().describe('The uid of the element to drag'),
108
+ to_uid: z.string().describe('The uid of the element to drop into'),
109
109
  },
110
110
  handler: async (request, response, context) => {
111
111
  const fromHandle = await context.getElementByUid(request.params.from_uid);
@@ -135,7 +135,7 @@ export const fillForm = defineTool({
135
135
  schema: {
136
136
  elements: z
137
137
  .array(z.object({
138
- uid: z.number().describe('The uid of the element to fill out'),
138
+ uid: z.string().describe('The uid of the element to fill out'),
139
139
  value: z.string().describe('Value for the element'),
140
140
  }))
141
141
  .describe('Elements from snapshot to fill out.'),
@@ -165,7 +165,7 @@ export const uploadFile = defineTool({
165
165
  },
166
166
  schema: {
167
167
  uid: z
168
- .number()
168
+ .string()
169
169
  .describe('The uid of the file input element or an element that will open file chooser on the page from the page content snapshot'),
170
170
  filePath: z.string().describe('The local path of the file to upload'),
171
171
  },
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import z from 'zod';
7
7
  import { defineTool } from './ToolDefinition.js';
8
- import { insightOutput, parseRawTraceBuffer } from '../trace-processing/parse.js';
8
+ import { getInsightOutput, getTraceSummary, parseRawTraceBuffer, } 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({
@@ -91,13 +91,40 @@ export const stopTrace = defineTool({
91
91
  await stopTracingAndAppendOutput(page, response, context);
92
92
  },
93
93
  });
94
+ export const analyzeInsight = defineTool({
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',
97
+ annotations: {
98
+ category: ToolCategories.PERFORMANCE,
99
+ readOnlyHint: true,
100
+ },
101
+ schema: {
102
+ insightName: z
103
+ .string()
104
+ .describe('The name of the Insight you want more information on. For example: "DocumentLatency" or "LCPBreakdown"'),
105
+ },
106
+ handler: async (request, response, context) => {
107
+ const lastRecording = context.recordedTraces().at(-1);
108
+ if (!lastRecording) {
109
+ response.appendResponseLine('No recorded traces found. Record a performance trace so you have Insights to analyze.');
110
+ return;
111
+ }
112
+ const insightOutput = getInsightOutput(lastRecording, request.params.insightName);
113
+ if ('error' in insightOutput) {
114
+ response.appendResponseLine(insightOutput.error);
115
+ return;
116
+ }
117
+ response.appendResponseLine(insightOutput.output);
118
+ },
119
+ });
94
120
  async function stopTracingAndAppendOutput(page, response, context) {
95
121
  try {
96
122
  const traceEventsBuffer = await page.tracing.stop();
97
123
  const result = await parseRawTraceBuffer(traceEventsBuffer);
98
124
  response.appendResponseLine('The performance trace has been stopped.');
99
125
  if (result) {
100
- const insightText = insightOutput(result);
126
+ context.storeTraceRecording(result);
127
+ const insightText = getTraceSummary(result);
101
128
  if (insightText) {
102
129
  response.appendResponseLine('Insights with performance opportunities:');
103
130
  response.appendResponseLine(insightText);
@@ -19,7 +19,7 @@ export const screenshot = defineTool({
19
19
  .default('png')
20
20
  .describe('Type of format to save the screenshot as. Default is "png"'),
21
21
  uid: z
22
- .number()
22
+ .string()
23
23
  .optional()
24
24
  .describe('The uid of an element on the page from the page content snapshot. If omitted takes a pages screenshot.'),
25
25
  fullPage: z
@@ -9,27 +9,53 @@ import { ToolCategories } from './categories.js';
9
9
  import { waitForEventsAfterAction } from '../waitForHelpers.js';
10
10
  export const evaluateScript = defineTool({
11
11
  name: 'evaluate_script',
12
- description: `Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON.`,
12
+ description: `Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON
13
+ so returned values have to JSON-serializable.`,
13
14
  annotations: {
14
15
  category: ToolCategories.DEBUGGING,
15
16
  readOnlyHint: false,
16
17
  },
17
18
  schema: {
18
- function: z
19
- .string()
20
- .describe('A JavaScript function to run in the currently selected page. Example: `() => {return document.title}` or `async () => {return await fetch("example.com")}`'),
19
+ function: z.string().describe(`A JavaScript function to run in the currently selected page.
20
+ Example without arguments: \`() => {
21
+ return document.title
22
+ }\` or \`async () => {
23
+ return await fetch("example.com")
24
+ }\`.
25
+ Example with arguments: \`(el) => {
26
+ return el.innerText;
27
+ }\`
28
+ `),
29
+ args: z
30
+ .array(z.object({
31
+ uid: z
32
+ .string()
33
+ .describe('The uid of an element on the page from the page content snapshot'),
34
+ }))
35
+ .optional()
36
+ .describe(`An optional list of arguments to pass to the function.`),
21
37
  },
22
38
  handler: async (request, response, context) => {
23
39
  const page = context.getSelectedPage();
24
- const script = `(async () => {
25
- return JSON.stringify(await (${request.params.function})());
26
- })()`;
27
- await waitForEventsAfterAction(page, async () => {
28
- const result = await page.evaluate(script);
29
- response.appendResponseLine('Script ran on page and returned:');
30
- response.appendResponseLine('```json');
31
- response.appendResponseLine(`${result}`);
32
- response.appendResponseLine('```');
33
- });
40
+ const fn = await page.evaluateHandle(`(${request.params.function})`);
41
+ const args = [fn];
42
+ try {
43
+ for (const el of request.params.args ?? []) {
44
+ args.push(await context.getElementByUid(el.uid));
45
+ }
46
+ await waitForEventsAfterAction(page, async () => {
47
+ const result = await page.evaluate(async (fn, ...args) => {
48
+ // @ts-expect-error no types.
49
+ return JSON.stringify(await fn(...args));
50
+ }, ...args);
51
+ response.appendResponseLine('Script ran on page and returned:');
52
+ response.appendResponseLine('```json');
53
+ response.appendResponseLine(`${result}`);
54
+ response.appendResponseLine('```');
55
+ });
56
+ }
57
+ finally {
58
+ Promise.allSettled(args.map(arg => arg.dispose())).catch(() => { });
59
+ }
34
60
  },
35
61
  });
@@ -3,9 +3,11 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
+ import { PerformanceTraceFormatter } from '../../node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js';
6
7
  import { PerformanceInsightFormatter } from '../../node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js';
7
8
  import * as TraceEngine from '../../node_modules/chrome-devtools-frontend/front_end/models/trace/trace.js';
8
9
  import { logger } from '../logger.js';
10
+ import { AgentFocus } from '../../node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js';
9
11
  const engine = TraceEngine.TraceModel.Model.createWithAllHandlers();
10
12
  export async function parseRawTraceBuffer(buffer) {
11
13
  engine.resetProcessor();
@@ -43,31 +45,33 @@ export async function parseRawTraceBuffer(buffer) {
43
45
  return null;
44
46
  }
45
47
  }
46
- // TODO(jactkfranklin): move the formatters from DevTools to use here.
47
- // This is a very temporary helper to output some text from the tool call to aid development.
48
- export function insightOutput(result) {
48
+ export function getTraceSummary(result) {
49
+ const focus = AgentFocus.full(result.parsedTrace);
50
+ const serializer = new TraceEngine.EventsSerializer.EventsSerializer();
51
+ const formatter = new PerformanceTraceFormatter(focus, serializer);
52
+ const output = formatter.formatTraceSummary();
53
+ return output;
54
+ }
55
+ export function getInsightOutput(result, insightName) {
56
+ // Currently, we do not support inspecting traces with multiple navigations. We either:
57
+ // 1. Find Insights from the first navigation (common case: user records a trace with a page reload to test load performance)
58
+ // 2. Fall back to finding Insights not associated with a navigation (common case: user tests an interaction without a page load).
49
59
  const mainNavigationId = result.parsedTrace.data.Meta.mainFrameNavigations.at(0)?.args.data
50
60
  ?.navigationId;
51
- if (!mainNavigationId) {
52
- return '';
53
- }
54
- let text = '';
55
- const insightsForNav = result.insights.get(mainNavigationId);
61
+ const insightsForNav = result.insights.get(mainNavigationId ?? TraceEngine.Types.Events.NO_NAVIGATION);
56
62
  if (!insightsForNav) {
57
- text += 'No Performance insights were found for this trace.';
58
- return text;
63
+ return {
64
+ error: 'No Performance Insights for this trace.',
65
+ };
59
66
  }
60
- const failingInsightKeys = Object.keys(insightsForNav.model).filter(insightKey => {
61
- const key = insightKey;
62
- const data = insightsForNav.model[key] ?? null;
63
- return data?.state === 'fail';
64
- });
65
- logger(`Found failing Insight keys: ${failingInsightKeys.join(', ')}`);
66
- for (const failingKey of failingInsightKeys) {
67
- const modelData = insightsForNav.model[failingKey];
68
- const formatter = new PerformanceInsightFormatter(result.parsedTrace, modelData);
69
- const output = formatter.formatInsight();
70
- text += `${output}\n`;
67
+ const matchingInsight = insightName in insightsForNav.model
68
+ ? insightsForNav.model[insightName]
69
+ : null;
70
+ if (!matchingInsight) {
71
+ return {
72
+ error: `No Insight with the name ${insightName} found. Double check the name you provided is accurate and try again.`,
73
+ };
71
74
  }
72
- return text;
75
+ const formatter = new PerformanceInsightFormatter(result.parsedTrace, matchingInsight);
76
+ return { output: formatter.formatInsight() };
73
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp",
3
- "version": "0.0.2",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for Chrome DevTools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,14 +9,16 @@
9
9
  "main": "index.js",
10
10
  "scripts": {
11
11
  "build": "tsc && node --experimental-strip-types scripts/post-build.ts",
12
+ "typecheck": "tsc --noEmit",
12
13
  "format": "eslint --cache --fix . ;prettier --write --cache .",
13
14
  "check-format": "eslint --cache .; prettier --check --cache .;",
14
15
  "generate-docs": "npm run build && node --experimental-strip-types scripts/generate-docs.ts",
15
16
  "start": "npm run build && node build/src/index.js",
16
17
  "start-debug": "DEBUG=mcp:* DEBUG_COLORS=false npm run build && node build/src/index.js",
17
- "test": "npm run build && node --test-reporter spec --test-force-exit --test 'build/tests/**/*.test.js'",
18
- "test:only": "npm run build && node --test-reporter spec --test-force-exit --test --test-only 'build/tests/**/*.test.js'",
19
- "test:only:no-build": "node --test-reporter spec --test-force-exit --test --test-only 'build/tests/**/*.test.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'",
20
22
  "prepare": "node --experimental-strip-types scripts/prepare.ts"
21
23
  },
22
24
  "files": [
@@ -35,7 +37,7 @@
35
37
  "dependencies": {
36
38
  "@modelcontextprotocol/sdk": "1.18.0",
37
39
  "debug": "4.4.3",
38
- "puppeteer-core": "24.21.0",
40
+ "puppeteer-core": "24.22.0",
39
41
  "yargs": "18.0.0"
40
42
  },
41
43
  "devDependencies": {
@@ -47,11 +49,11 @@
47
49
  "@types/yargs": "^17.0.33",
48
50
  "@typescript-eslint/eslint-plugin": "^8.43.0",
49
51
  "@typescript-eslint/parser": "^8.43.0",
50
- "chrome-devtools-frontend": "1.0.1514545",
52
+ "chrome-devtools-frontend": "1.0.1515796",
51
53
  "eslint": "^9.35.0",
52
54
  "globals": "^16.4.0",
53
55
  "prettier": "^3.6.2",
54
- "puppeteer": "24.21.0",
56
+ "puppeteer": "24.22.0",
55
57
  "sinon": "^21.0.0",
56
58
  "typescript": "^5.9.2",
57
59
  "typescript-eslint": "^8.43.0"