chrome-devtools-mcp 1.0.1 → 1.1.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.
@@ -30,7 +30,7 @@ export function definePageTool(definition) {
30
30
  }
31
31
  export const CLOSE_PAGE_ERROR = 'The last open page cannot be closed. It is fine to keep it open.';
32
32
  export const pageIdSchema = {
33
- pageId: zod.number().optional().describe('Targets a specific page by ID.'),
33
+ pageId: zod.number().describe('Targets a specific page by ID.'),
34
34
  };
35
35
  export const timeoutSchema = {
36
36
  timeout: zod
@@ -7,6 +7,26 @@
7
7
  import { zod, PredefinedNetworkConditions } from '../third_party/index.js';
8
8
  import { ToolCategory } from './categories.js';
9
9
  import { definePageTool, geolocationTransform, viewportTransform, } from './ToolDefinition.js';
10
+ function headerStringTransform(value) {
11
+ if (value === undefined) {
12
+ return undefined;
13
+ }
14
+ if (value === '') {
15
+ return {};
16
+ }
17
+ try {
18
+ const parsed = JSON.parse(value);
19
+ if (typeof parsed !== 'object' ||
20
+ parsed === null ||
21
+ Array.isArray(parsed)) {
22
+ throw new Error('Headers must be a JSON object');
23
+ }
24
+ return parsed;
25
+ }
26
+ catch (error) {
27
+ throw new Error(`Invalid JSON for headers: ${error instanceof Error ? error.message : String(error)}`);
28
+ }
29
+ }
10
30
  const throttlingOptions = [
11
31
  'Offline',
12
32
  ...Object.keys(PredefinedNetworkConditions),
@@ -47,6 +67,11 @@ export const emulate = definePageTool({
47
67
  .optional()
48
68
  .transform(viewportTransform)
49
69
  .describe(`Emulate device viewports '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]'. 'touch' and 'mobile' to emulate mobile devices. 'landscape' to emulate landscape mode.`),
70
+ extraHttpHeaders: zod
71
+ .string()
72
+ .optional()
73
+ .transform(headerStringTransform)
74
+ .describe('Extra HTTP headers as a JSON string object, e.g. {"X-Custom": "value", "Authorization": "Bearer token"}. Headers are included into every HTTP request originating from the page and persist across navigations until cleared. Pass an empty string to clear all extra headers.'),
50
75
  },
51
76
  blockedByDialog: true,
52
77
  handler: async (request, response, context) => {
@@ -21,6 +21,7 @@ export const installExtension = defineTool({
21
21
  blockedByDialog: false,
22
22
  handler: async (request, response, context) => {
23
23
  const { path } = request.params;
24
+ await context.validatePath(path);
24
25
  const id = await context.installExtension(path);
25
26
  response.appendResponseLine(`Extension installed. Id: ${id}`);
26
27
  },
@@ -72,6 +73,7 @@ export const reloadExtension = defineTool({
72
73
  if (!extension) {
73
74
  throw new Error(`Extension with ID ${id} not found.`);
74
75
  }
76
+ await context.validatePath(extension.path);
75
77
  await context.installExtension(extension.path);
76
78
  response.appendResponseLine('Extension reloaded.');
77
79
  },
@@ -393,7 +393,7 @@ export const uploadFile = definePageTool({
393
393
  blockedByDialog: true,
394
394
  handler: async (request, response, context) => {
395
395
  const { uid, filePath } = request.params;
396
- context.validatePath(filePath);
396
+ await context.validatePath(filePath);
397
397
  const handle = (await request.page.getElementByUid(uid));
398
398
  try {
399
399
  try {
@@ -40,7 +40,7 @@ export const lighthouseAudit = definePageTool({
40
40
  ];
41
41
  const formats = ['json', 'html'];
42
42
  const { mode = 'navigation', device = 'desktop', outputDirPath, } = request.params;
43
- context.validatePath(outputDirPath);
43
+ await context.validatePath(outputDirPath);
44
44
  const flags = {
45
45
  onlyCategories: categories,
46
46
  output: formats,
@@ -7,8 +7,8 @@ import { zod } from '../third_party/index.js';
7
7
  import { ensureExtension } from '../utils/files.js';
8
8
  import { ToolCategory } from './categories.js';
9
9
  import { definePageTool, defineTool } from './ToolDefinition.js';
10
- export const takeMemorySnapshot = definePageTool({
11
- name: 'take_memory_snapshot',
10
+ export const takeHeapSnapshot = definePageTool({
11
+ name: 'take_heapsnapshot',
12
12
  description: `Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.`,
13
13
  annotations: {
14
14
  category: ToolCategory.MEMORY,
@@ -22,15 +22,15 @@ export const takeMemorySnapshot = definePageTool({
22
22
  blockedByDialog: true,
23
23
  handler: async (request, response, context) => {
24
24
  const page = request.page;
25
- context.validatePath(request.params.filePath);
25
+ await context.validatePath(request.params.filePath);
26
26
  await page.pptrPage.captureHeapSnapshot({
27
27
  path: ensureExtension(request.params.filePath, '.heapsnapshot'),
28
28
  });
29
29
  response.appendResponseLine(`Heap snapshot saved to ${request.params.filePath}`);
30
30
  },
31
31
  });
32
- export const exploreMemorySnapshot = defineTool({
33
- name: 'load_memory_snapshot',
32
+ export const getHeapSnapshotSummary = defineTool({
33
+ name: 'get_heapsnapshot_summary',
34
34
  description: 'Loads a memory heapsnapshot and returns snapshot summary stats.',
35
35
  annotations: {
36
36
  category: ToolCategory.MEMORY,
@@ -42,14 +42,14 @@ export const exploreMemorySnapshot = defineTool({
42
42
  },
43
43
  blockedByDialog: false,
44
44
  handler: async (request, response, context) => {
45
- context.validatePath(request.params.filePath);
45
+ await context.validatePath(request.params.filePath);
46
46
  const stats = await context.getHeapSnapshotStats(request.params.filePath);
47
47
  const staticData = await context.getHeapSnapshotStaticData(request.params.filePath);
48
48
  response.setHeapSnapshotStats(stats, staticData);
49
49
  },
50
50
  });
51
- export const getMemorySnapshotDetails = defineTool({
52
- name: 'get_memory_snapshot_details',
51
+ export const getHeapSnapshotDetails = defineTool({
52
+ name: 'get_heapsnapshot_details',
53
53
  description: 'Loads a memory heapsnapshot and returns all available information including statistics, static data, and aggregated node information. Supports pagination for aggregates.',
54
54
  annotations: {
55
55
  category: ToolCategory.MEMORY,
@@ -69,7 +69,7 @@ export const getMemorySnapshotDetails = defineTool({
69
69
  },
70
70
  blockedByDialog: false,
71
71
  handler: async (request, response, context) => {
72
- context.validatePath(request.params.filePath);
72
+ await context.validatePath(request.params.filePath);
73
73
  const aggregates = await context.getHeapSnapshotAggregates(request.params.filePath);
74
74
  response.setHeapSnapshotAggregates(aggregates, {
75
75
  pageIdx: request.params.pageIdx,
@@ -77,9 +77,9 @@ export const getMemorySnapshotDetails = defineTool({
77
77
  });
78
78
  },
79
79
  });
80
- export const getNodesByClass = defineTool({
81
- name: 'get_nodes_by_class',
82
- description: 'Loads a memory heapsnapshot and returns instances of a specific class with their stable IDs.',
80
+ export const getHeapSnapshotClassNodes = defineTool({
81
+ name: 'get_heapsnapshot_class_nodes',
82
+ description: 'Loads a memory heapsnapshot and returns instances of a specific class with their IDs.',
83
83
  annotations: {
84
84
  category: ToolCategory.MEMORY,
85
85
  readOnlyHint: true,
@@ -87,24 +87,22 @@ export const getNodesByClass = defineTool({
87
87
  },
88
88
  schema: {
89
89
  filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
90
- uid: zod
91
- .number()
92
- .describe('The unique UID for the class, obtained from aggregates listing.'),
90
+ id: zod.number().describe('The ID for the class, obtained from details.'),
93
91
  pageIdx: zod.number().optional().describe('The page index for pagination.'),
94
92
  pageSize: zod.number().optional().describe('The page size for pagination.'),
95
93
  },
96
94
  blockedByDialog: false,
97
95
  handler: async (request, response, context) => {
98
- context.validatePath(request.params.filePath);
99
- const nodes = await context.getHeapSnapshotNodesByUid(request.params.filePath, request.params.uid);
96
+ await context.validatePath(request.params.filePath);
97
+ const nodes = await context.getHeapSnapshotNodesById(request.params.filePath, request.params.id);
100
98
  response.setHeapSnapshotNodes(nodes, {
101
99
  pageIdx: request.params.pageIdx,
102
100
  pageSize: request.params.pageSize,
103
101
  });
104
102
  },
105
103
  });
106
- export const getNodeRetainers = defineTool({
107
- name: 'get_node_retainers',
104
+ export const getHeapSnapshotRetainers = defineTool({
105
+ name: 'get_heapsnapshot_retainers',
108
106
  description: 'Loads a memory heapsnapshot and returns retainers for a specific node ID.',
109
107
  annotations: {
110
108
  category: ToolCategory.MEMORY,
@@ -114,12 +112,12 @@ export const getNodeRetainers = defineTool({
114
112
  blockedByDialog: false,
115
113
  schema: {
116
114
  filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
117
- nodeId: zod.number().describe('The stable node ID to get retainers for.'),
115
+ nodeId: zod.number().describe('The node ID to get retainers for.'),
118
116
  pageIdx: zod.number().optional().describe('The page index for pagination.'),
119
117
  pageSize: zod.number().optional().describe('The page size for pagination.'),
120
118
  },
121
119
  handler: async (request, response, context) => {
122
- context.validatePath(request.params.filePath);
120
+ await context.validatePath(request.params.filePath);
123
121
  const retainers = await context.getHeapSnapshotRetainers(request.params.filePath, request.params.nodeId);
124
122
  response.setHeapSnapshotNodes(retainers, {
125
123
  pageIdx: request.params.pageIdx,
@@ -96,8 +96,8 @@ export const getNetworkRequest = definePageTool({
96
96
  },
97
97
  blockedByDialog: true,
98
98
  handler: async (request, response, context) => {
99
- context.validatePath(request.params.requestFilePath);
100
- context.validatePath(request.params.responseFilePath);
99
+ await context.validatePath(request.params.requestFilePath);
100
+ await context.validatePath(request.params.responseFilePath);
101
101
  if (request.params.reqid) {
102
102
  response.attachNetworkRequest(request.params.reqid, {
103
103
  requestFilePath: request.params.requestFilePath,
@@ -33,7 +33,7 @@ export const startTrace = definePageTool({
33
33
  },
34
34
  blockedByDialog: true,
35
35
  handler: async (request, response, context) => {
36
- context.validatePath(request.params.filePath);
36
+ await context.validatePath(request.params.filePath);
37
37
  if (context.isRunningPerformanceTrace()) {
38
38
  response.appendResponseLine('Error: a performance trace is already running. Use performance_stop_trace to stop it. Only one trace can be running at any given time.');
39
39
  return;
@@ -78,7 +78,7 @@ export const startTrace = definePageTool({
78
78
  }
79
79
  if (request.params.autoStop) {
80
80
  await new Promise(resolve => setTimeout(resolve, 5_000));
81
- await stopTracingAndAppendOutput(page.pptrPage, response, context, request.params.filePath);
81
+ await stopTracingAndAppendOutput(page, response, context, request.params.filePath);
82
82
  }
83
83
  else {
84
84
  response.appendResponseLine(`The performance trace is being recorded. Use performance_stop_trace to stop it.`);
@@ -97,12 +97,12 @@ export const stopTrace = definePageTool({
97
97
  },
98
98
  blockedByDialog: true,
99
99
  handler: async (request, response, context) => {
100
- context.validatePath(request.params.filePath);
100
+ await context.validatePath(request.params.filePath);
101
101
  if (!context.isRunningPerformanceTrace()) {
102
102
  return;
103
103
  }
104
104
  const page = request.page;
105
- await stopTracingAndAppendOutput(page.pptrPage, response, context, request.params.filePath);
105
+ await stopTracingAndAppendOutput(page, response, context, request.params.filePath);
106
106
  },
107
107
  });
108
108
  export const analyzeInsight = definePageTool({
@@ -132,7 +132,7 @@ export const analyzeInsight = definePageTool({
132
132
  });
133
133
  async function stopTracingAndAppendOutput(page, response, context, filePath) {
134
134
  try {
135
- const traceEventsBuffer = await page.tracing.stop();
135
+ const traceEventsBuffer = await page.pptrPage.tracing.stop();
136
136
  if (filePath && traceEventsBuffer) {
137
137
  let dataToWrite = traceEventsBuffer;
138
138
  if (filePath.endsWith('.gz')) {
@@ -150,7 +150,10 @@ async function stopTracingAndAppendOutput(page, response, context, filePath) {
150
150
  const file = await context.saveFile(dataToWrite, filePath, filePath.endsWith('.gz') ? '.json.gz' : '.json');
151
151
  response.appendResponseLine(`The raw trace data was saved to ${file.filename}.`);
152
152
  }
153
- const result = await parseRawTraceBuffer(traceEventsBuffer);
153
+ const result = await parseRawTraceBuffer(traceEventsBuffer, {
154
+ cpuThrottling: page.cpuThrottlingRate,
155
+ networkThrottling: page.networkConditions ?? undefined,
156
+ });
154
157
  response.appendResponseLine('The performance trace has been stopped.');
155
158
  if (traceResultIsSuccess(result)) {
156
159
  if (context.isCruxEnabled()) {
@@ -31,7 +31,7 @@ export const startScreencast = definePageTool(args => ({
31
31
  },
32
32
  blockedByDialog: false,
33
33
  handler: async (request, response, context) => {
34
- context.validatePath(request.params.filePath);
34
+ await context.validatePath(request.params.filePath);
35
35
  if (context.getScreenRecorder() !== null) {
36
36
  response.appendResponseLine('Error: a screencast recording is already in progress. Use screencast_stop to stop it before starting a new one.');
37
37
  return;
@@ -40,7 +40,7 @@ export const screenshot = definePageTool({
40
40
  },
41
41
  blockedByDialog: true,
42
42
  handler: async (request, response, context) => {
43
- context.validatePath(request.params.filePath);
43
+ await context.validatePath(request.params.filePath);
44
44
  if (request.params.uid && request.params.fullPage) {
45
45
  throw new Error('Providing both "uid" and "fullPage" is not allowed.');
46
46
  }
@@ -53,7 +53,7 @@ Example with arguments: \`(el) => {
53
53
  blockedByDialog: true,
54
54
  handler: async (request, response, context) => {
55
55
  const { serviceWorkerId, args: uidArgs, function: fnString, pageId, dialogAction, filePath, } = request.params;
56
- context.validatePath(filePath);
56
+ await context.validatePath(filePath);
57
57
  if (cliArgs?.categoryExtensions && serviceWorkerId) {
58
58
  if (uidArgs && uidArgs.length > 0) {
59
59
  throw new Error('args (element uids) cannot be used when evaluating in a service worker.');
@@ -28,7 +28,7 @@ in the DevTools Elements panel (if any).`,
28
28
  },
29
29
  blockedByDialog: true,
30
30
  handler: async (request, response, context) => {
31
- context.validatePath(request.params.filePath);
31
+ await context.validatePath(request.params.filePath);
32
32
  response.includeSnapshot({
33
33
  verbose: request.params.verbose ?? false,
34
34
  filePath: request.params.filePath,
@@ -9,7 +9,7 @@ const engine = DevTools.TraceEngine.TraceModel.Model.createWithAllHandlers();
9
9
  export function traceResultIsSuccess(x) {
10
10
  return 'parsedTrace' in x;
11
11
  }
12
- export async function parseRawTraceBuffer(buffer) {
12
+ export async function parseRawTraceBuffer(buffer, metadata) {
13
13
  engine.resetProcessor();
14
14
  if (!buffer) {
15
15
  return {
@@ -25,7 +25,7 @@ export async function parseRawTraceBuffer(buffer) {
25
25
  try {
26
26
  const data = JSON.parse(asString);
27
27
  const events = Array.isArray(data) ? data : data.traceEvents;
28
- await engine.parse(events);
28
+ await engine.parse(events, { metadata });
29
29
  const parsedTrace = engine.parsedTrace();
30
30
  if (!parsedTrace) {
31
31
  return {
@@ -15,4 +15,47 @@ export function ensureExtension(filepath, extension) {
15
15
  const ext = path.extname(filepath);
16
16
  return filepath.slice(0, filepath.length - ext.length) + extension;
17
17
  }
18
+ export async function resolveCanonicalPath(filePath) {
19
+ const absolutePath = path.resolve(filePath);
20
+ try {
21
+ // Get the true canonical path, resolving all symlinks.
22
+ return await fs.realpath(absolutePath);
23
+ }
24
+ catch (err) {
25
+ if (err &&
26
+ typeof err === 'object' &&
27
+ 'code' in err &&
28
+ err.code === 'ENOENT') {
29
+ // Find the nearest existing ancestor directory on the filesystem.
30
+ let current = absolutePath;
31
+ const missingSegments = [];
32
+ while (true) {
33
+ const parent = path.dirname(current);
34
+ if (parent === current) {
35
+ // Reached root directory but still couldn't resolve anything.
36
+ throw err;
37
+ }
38
+ try {
39
+ const canonicalParent = await fs.realpath(parent);
40
+ return path.join(canonicalParent, path.basename(current), ...missingSegments);
41
+ }
42
+ catch (parentErr) {
43
+ if (parentErr &&
44
+ typeof parentErr === 'object' &&
45
+ 'code' in parentErr &&
46
+ parentErr.code === 'ENOENT') {
47
+ missingSegments.unshift(path.basename(current));
48
+ current = parent;
49
+ }
50
+ else {
51
+ throw parentErr;
52
+ }
53
+ }
54
+ }
55
+ }
56
+ else {
57
+ throw err;
58
+ }
59
+ }
60
+ }
18
61
  //# sourceMappingURL=files.js.map
@@ -5,6 +5,6 @@
5
5
  */
6
6
  // If moved update release-please config
7
7
  // x-release-please-start-version
8
- export const VERSION = '1.0.1';
8
+ export const VERSION = '1.1.0';
9
9
  // x-release-please-end
10
10
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for Chrome DevTools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,7 +62,7 @@
62
62
  "@types/yargs": "^17.0.33",
63
63
  "@typescript-eslint/eslint-plugin": "^8.43.0",
64
64
  "@typescript-eslint/parser": "^8.43.0",
65
- "chrome-devtools-frontend": "1.0.1631386",
65
+ "chrome-devtools-frontend": "1.0.1632065",
66
66
  "core-js": "3.49.0",
67
67
  "debug": "4.4.3",
68
68
  "eslint": "^9.35.0",
@@ -71,7 +71,7 @@
71
71
  "globals": "^17.0.0",
72
72
  "lighthouse": "13.3.0",
73
73
  "prettier": "^3.6.2",
74
- "puppeteer": "25.0.4",
74
+ "puppeteer": "25.1.0",
75
75
  "rollup": "4.60.4",
76
76
  "rollup-plugin-cleanup": "^3.2.1",
77
77
  "rollup-plugin-license": "^3.6.0",