Skip to content

Commit fadbf41

Browse files
authored
feat: Add query_heapsnapshot MCP tool (#2553)
`query_heapsnapshot_objects` allows the agent to filter objects by properties like class name, self size, retained size, property name and/or detachedness. The resulting list of objects can be sorted as well.
1 parent 1c92ba0 commit fadbf41

10 files changed

Lines changed: 413 additions & 3 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
557557
- [`take_snapshot`](docs/tool-reference.md#take_snapshot)
558558
- [`screencast_start`](docs/tool-reference.md#screencast_start)
559559
- [`screencast_stop`](docs/tool-reference.md#screencast_stop)
560-
- **Memory** (12 tools)
560+
- **Memory** (13 tools)
561561
- [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
562562
- [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
563563
- [`compare_heapsnapshots`](docs/tool-reference.md#compare_heapsnapshots)
@@ -570,6 +570,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
570570
- [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
571571
- [`get_heapsnapshot_retaining_paths`](docs/tool-reference.md#get_heapsnapshot_retaining_paths)
572572
- [`get_heapsnapshot_summary`](docs/tool-reference.md#get_heapsnapshot_summary)
573+
- [`query_heapsnapshot_objects`](docs/tool-reference.md#query_heapsnapshot_objects)
573574
- **Extensions** (5 tools)
574575
- [`install_extension`](docs/tool-reference.md#install_extension)
575576
- [`list_extensions`](docs/tool-reference.md#list_extensions)

docs/tool-reference.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
- [`take_snapshot`](#take_snapshot)
4040
- [`screencast_start`](#screencast_start)
4141
- [`screencast_stop`](#screencast_stop)
42-
- **[Memory](#memory)** (12 tools)
42+
- **[Memory](#memory)** (13 tools)
4343
- [`take_heapsnapshot`](#take_heapsnapshot)
4444
- [`close_heapsnapshot`](#close_heapsnapshot)
4545
- [`compare_heapsnapshots`](#compare_heapsnapshots)
@@ -52,6 +52,7 @@
5252
- [`get_heapsnapshot_retainers`](#get_heapsnapshot_retainers)
5353
- [`get_heapsnapshot_retaining_paths`](#get_heapsnapshot_retaining_paths)
5454
- [`get_heapsnapshot_summary`](#get_heapsnapshot_summary)
55+
- [`query_heapsnapshot_objects`](#query_heapsnapshot_objects)
5556
- **[Extensions](#extensions)** (5 tools)
5657
- [`install_extension`](#install_extension)
5758
- [`list_extensions`](#list_extensions)
@@ -600,6 +601,27 @@ in the DevTools Elements panel (if any).
600601

601602
---
602603

604+
### `query_heapsnapshot_objects`
605+
606+
**Description:** Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, minRetainedSize, maxRetainedSize, minSelfSize, isDetached, sortBy). (requires flag: --memoryDebugging=true)
607+
608+
**Parameters:**
609+
610+
- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
611+
- **className** (string) _(optional)_: Optional regex or text matching object class name.
612+
- **isDetached** (boolean) _(optional)_: Whether to filter for detached DOM nodes.
613+
- **maxRetainedSize** (number) _(optional)_: Maximum retained size in bytes.
614+
- **maxSelfSize** (number) _(optional)_: Maximum self size in bytes.
615+
- **minRetainedSize** (number) _(optional)_: Minimum retained size in bytes.
616+
- **minSelfSize** (number) _(optional)_: Minimum self size in bytes.
617+
- **nodeType** (string) _(optional)_: Optional V8 node type filter (e.g. object, closure, string, array, code).
618+
- **pageIdx** (number) _(optional)_: The page index for pagination.
619+
- **pageSize** (number) _(optional)_: The page size for pagination.
620+
- **propertyName** (string) _(optional)_: Optional property name filter for outgoing reference edges.
621+
- **sortBy** (enum: "retainedSize", "selfSize", "id") _(optional)_: Sort order for results. Default is retainedSize.
622+
623+
---
624+
603625
## Extensions
604626

605627
> NOTE: The Extensions category is not active by default. Use the '--categoryExtensions' flag.

src/McpContext.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
HeapSnapshotDetailedClassDiff,
1818
DuplicateStringGroup,
1919
HeapEdgesQueryOptions,
20+
HeapQueryOptions,
2021
} from './processors/HeapSnapshotManager.js';
2122
import {McpPage} from './McpPage.js';
2223
import {type UncaughtError} from './collectors/PageCollector.js';
@@ -750,6 +751,13 @@ export class McpContext implements Context {
750751
return await this.#heapSnapshotManager.getDuplicateStrings(filePath);
751752
}
752753

754+
async queryHeapSnapshotObjects(
755+
filePath: string,
756+
options: HeapQueryOptions,
757+
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
758+
return await this.#heapSnapshotManager.queryObjects(filePath, options);
759+
}
760+
753761
async getHeapSnapshotStats(
754762
filePath: string,
755763
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics> {

src/config/cli-options.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,88 @@ export const commands: Commands = {
11001100
},
11011101
},
11021102
},
1103+
query_heapsnapshot_objects: {
1104+
description:
1105+
'Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, minRetainedSize, maxRetainedSize, minSelfSize, isDetached, sortBy). (requires flag: --memoryDebugging=true)',
1106+
category: 'Memory',
1107+
args: {
1108+
filePath: {
1109+
name: 'filePath',
1110+
type: 'string',
1111+
description: 'A path to a .heapsnapshot file to read.',
1112+
required: true,
1113+
},
1114+
className: {
1115+
name: 'className',
1116+
type: 'string',
1117+
description: 'Optional regex or text matching object class name.',
1118+
required: false,
1119+
},
1120+
propertyName: {
1121+
name: 'propertyName',
1122+
type: 'string',
1123+
description:
1124+
'Optional property name filter for outgoing reference edges.',
1125+
required: false,
1126+
},
1127+
nodeType: {
1128+
name: 'nodeType',
1129+
type: 'string',
1130+
description:
1131+
'Optional V8 node type filter (e.g. object, closure, string, array, code).',
1132+
required: false,
1133+
},
1134+
minRetainedSize: {
1135+
name: 'minRetainedSize',
1136+
type: 'number',
1137+
description: 'Minimum retained size in bytes.',
1138+
required: false,
1139+
},
1140+
maxRetainedSize: {
1141+
name: 'maxRetainedSize',
1142+
type: 'number',
1143+
description: 'Maximum retained size in bytes.',
1144+
required: false,
1145+
},
1146+
minSelfSize: {
1147+
name: 'minSelfSize',
1148+
type: 'number',
1149+
description: 'Minimum self size in bytes.',
1150+
required: false,
1151+
},
1152+
maxSelfSize: {
1153+
name: 'maxSelfSize',
1154+
type: 'number',
1155+
description: 'Maximum self size in bytes.',
1156+
required: false,
1157+
},
1158+
isDetached: {
1159+
name: 'isDetached',
1160+
type: 'boolean',
1161+
description: 'Whether to filter for detached DOM nodes.',
1162+
required: false,
1163+
},
1164+
sortBy: {
1165+
name: 'sortBy',
1166+
type: 'string',
1167+
description: 'Sort order for results. Default is retainedSize.',
1168+
required: false,
1169+
enum: ['retainedSize', 'selfSize', 'id'],
1170+
},
1171+
pageIdx: {
1172+
name: 'pageIdx',
1173+
type: 'number',
1174+
description: 'The page index for pagination.',
1175+
required: false,
1176+
},
1177+
pageSize: {
1178+
name: 'pageSize',
1179+
type: 'number',
1180+
description: 'The page size for pagination.',
1181+
required: false,
1182+
},
1183+
},
1184+
},
11031185
reload_extension: {
11041186
description:
11051187
'Reloads an unpacked Chrome extension by its ID. (requires flag: --categoryExtensions=true)',

src/processors/HeapSnapshotManager.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export type HeapQueryOptions =
4848

4949
export type HeapEdgesQueryOptions =
5050
DevTools.HeapSnapshotModel.HeapSnapshotModel.HeapEdgesQueryOptions;
51-
5251
const VALID_EXTENSIONS: readonly string[] = ['.heapsnapshot', '.heaptimeline'];
5352

5453
function hasValidHeapSnapshotExtension(filePath: string): boolean {
@@ -423,6 +422,15 @@ export class HeapSnapshotManager {
423422
return await snapshot.getDuplicateStrings();
424423
}
425424

425+
async queryObjects(
426+
filePath: string,
427+
options: HeapQueryOptions,
428+
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
429+
const snapshot = await this.getSnapshot(filePath);
430+
const provider = snapshot.queryObjects(options);
431+
return await provider.serializeItemsRange(0, Infinity);
432+
}
433+
426434
hasSnapshots(): boolean {
427435
return this.#snapshots.size > 0;
428436
}

src/telemetry/tool_call_metrics.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,5 +978,58 @@
978978
"argType": "number"
979979
}
980980
]
981+
},
982+
{
983+
"name": "query_heapsnapshot_objects",
984+
"args": [
985+
{
986+
"name": "file_path_length",
987+
"argType": "number"
988+
},
989+
{
990+
"name": "class_name_length",
991+
"argType": "number"
992+
},
993+
{
994+
"name": "property_name_length",
995+
"argType": "number"
996+
},
997+
{
998+
"name": "node_type_length",
999+
"argType": "number"
1000+
},
1001+
{
1002+
"name": "min_retained_size",
1003+
"argType": "number"
1004+
},
1005+
{
1006+
"name": "max_retained_size",
1007+
"argType": "number"
1008+
},
1009+
{
1010+
"name": "min_self_size",
1011+
"argType": "number"
1012+
},
1013+
{
1014+
"name": "max_self_size",
1015+
"argType": "number"
1016+
},
1017+
{
1018+
"name": "is_detached",
1019+
"argType": "boolean"
1020+
},
1021+
{
1022+
"name": "sort_by",
1023+
"argType": "string"
1024+
},
1025+
{
1026+
"name": "page_idx",
1027+
"argType": "number"
1028+
},
1029+
{
1030+
"name": "page_size",
1031+
"argType": "number"
1032+
}
1033+
]
9811034
}
9821035
]

src/tools/ToolDefinition.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
HeapSnapshotDetailedClassDiff,
1212
DuplicateStringGroup,
1313
HeapEdgesQueryOptions,
14+
HeapQueryOptions,
1415
} from '../processors/HeapSnapshotManager.js';
1516
import type {McpPage} from '../McpPage.js';
1617
import {zod} from '../third_party/index.js';
@@ -315,6 +316,10 @@ export type Context = Readonly<{
315316
currentFilePath: string,
316317
classIndex: number,
317318
): Promise<HeapSnapshotDetailedClassDiff>;
319+
queryHeapSnapshotObjects(
320+
filePath: string,
321+
options: HeapQueryOptions,
322+
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
318323
}>;
319324

320325
/**

src/tools/memory.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,3 +465,80 @@ export const getHeapSnapshotObjectDetails = defineTool({
465465
response.setHeapSnapshotObjectDetails(objectInfo);
466466
},
467467
});
468+
469+
export const queryHeapSnapshotObjects = defineTool({
470+
name: 'query_heapsnapshot_objects',
471+
description:
472+
'Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, minRetainedSize, maxRetainedSize, minSelfSize, isDetached, sortBy).',
473+
annotations: {
474+
category: ToolCategory.MEMORY,
475+
readOnlyHint: true,
476+
conditions: ['memoryDebugging'],
477+
},
478+
blockedByDialog: false,
479+
verifyFilesSchema: {filePath: true},
480+
schema: {
481+
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
482+
className: zod
483+
.string()
484+
.optional()
485+
.describe('Optional regex or text matching object class name.'),
486+
propertyName: zod
487+
.string()
488+
.optional()
489+
.describe('Optional property name filter for outgoing reference edges.'),
490+
nodeType: zod
491+
.string()
492+
.optional()
493+
.describe(
494+
'Optional V8 node type filter (e.g. object, closure, string, array, code).',
495+
),
496+
minRetainedSize: zod
497+
.number()
498+
.optional()
499+
.describe('Minimum retained size in bytes.'),
500+
maxRetainedSize: zod
501+
.number()
502+
.optional()
503+
.describe('Maximum retained size in bytes.'),
504+
minSelfSize: zod
505+
.number()
506+
.optional()
507+
.describe('Minimum self size in bytes.'),
508+
maxSelfSize: zod
509+
.number()
510+
.optional()
511+
.describe('Maximum self size in bytes.'),
512+
isDetached: zod
513+
.boolean()
514+
.optional()
515+
.describe('Whether to filter for detached DOM nodes.'),
516+
sortBy: zod
517+
.enum(['retainedSize', 'selfSize', 'id'])
518+
.optional()
519+
.describe('Sort order for results. Default is retainedSize.'),
520+
pageIdx: zod.number().optional().describe('The page index for pagination.'),
521+
pageSize: zod.number().optional().describe('The page size for pagination.'),
522+
},
523+
handler: async (request, response, context) => {
524+
const range = await context.queryHeapSnapshotObjects(
525+
request.params.filePath,
526+
{
527+
className: request.params.className,
528+
propertyName: request.params.propertyName,
529+
nodeType: request.params.nodeType,
530+
minRetainedSize: request.params.minRetainedSize,
531+
maxRetainedSize: request.params.maxRetainedSize,
532+
minSelfSize: request.params.minSelfSize,
533+
maxSelfSize: request.params.maxSelfSize,
534+
isDetached: request.params.isDetached,
535+
sortBy: request.params.sortBy,
536+
},
537+
);
538+
539+
response.setHeapSnapshotNodes(range, {
540+
pageIdx: request.params.pageIdx,
541+
pageSize: request.params.pageSize,
542+
});
543+
},
544+
});

0 commit comments

Comments
 (0)