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.
This commit is contained in:
Dominik Inführ
2026-08-17 20:24:27 +00:00
committed by GitHub
parent 1c92ba091c
commit fadbf41d96
10 changed files with 413 additions and 3 deletions
+2 -1
View File
@@ -557,7 +557,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- [`take_snapshot`](docs/tool-reference.md#take_snapshot)
- [`screencast_start`](docs/tool-reference.md#screencast_start)
- [`screencast_stop`](docs/tool-reference.md#screencast_stop)
- **Memory** (12 tools)
- **Memory** (13 tools)
- [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
- [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
- [`compare_heapsnapshots`](docs/tool-reference.md#compare_heapsnapshots)
@@ -570,6 +570,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
- [`get_heapsnapshot_retaining_paths`](docs/tool-reference.md#get_heapsnapshot_retaining_paths)
- [`get_heapsnapshot_summary`](docs/tool-reference.md#get_heapsnapshot_summary)
- [`query_heapsnapshot_objects`](docs/tool-reference.md#query_heapsnapshot_objects)
- **Extensions** (5 tools)
- [`install_extension`](docs/tool-reference.md#install_extension)
- [`list_extensions`](docs/tool-reference.md#list_extensions)
+23 -1
View File
@@ -39,7 +39,7 @@
- [`take_snapshot`](#take_snapshot)
- [`screencast_start`](#screencast_start)
- [`screencast_stop`](#screencast_stop)
- **[Memory](#memory)** (12 tools)
- **[Memory](#memory)** (13 tools)
- [`take_heapsnapshot`](#take_heapsnapshot)
- [`close_heapsnapshot`](#close_heapsnapshot)
- [`compare_heapsnapshots`](#compare_heapsnapshots)
@@ -52,6 +52,7 @@
- [`get_heapsnapshot_retainers`](#get_heapsnapshot_retainers)
- [`get_heapsnapshot_retaining_paths`](#get_heapsnapshot_retaining_paths)
- [`get_heapsnapshot_summary`](#get_heapsnapshot_summary)
- [`query_heapsnapshot_objects`](#query_heapsnapshot_objects)
- **[Extensions](#extensions)** (5 tools)
- [`install_extension`](#install_extension)
- [`list_extensions`](#list_extensions)
@@ -600,6 +601,27 @@ in the DevTools Elements panel (if any).
---
### `query_heapsnapshot_objects`
**Description:** Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, minRetainedSize, maxRetainedSize, minSelfSize, isDetached, sortBy). (requires flag: --memoryDebugging=true)
**Parameters:**
- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
- **className** (string) _(optional)_: Optional regex or text matching object class name.
- **isDetached** (boolean) _(optional)_: Whether to filter for detached DOM nodes.
- **maxRetainedSize** (number) _(optional)_: Maximum retained size in bytes.
- **maxSelfSize** (number) _(optional)_: Maximum self size in bytes.
- **minRetainedSize** (number) _(optional)_: Minimum retained size in bytes.
- **minSelfSize** (number) _(optional)_: Minimum self size in bytes.
- **nodeType** (string) _(optional)_: Optional V8 node type filter (e.g. object, closure, string, array, code).
- **pageIdx** (number) _(optional)_: The page index for pagination.
- **pageSize** (number) _(optional)_: The page size for pagination.
- **propertyName** (string) _(optional)_: Optional property name filter for outgoing reference edges.
- **sortBy** (enum: "retainedSize", "selfSize", "id") _(optional)_: Sort order for results. Default is retainedSize.
---
## Extensions
> NOTE: The Extensions category is not active by default. Use the '--categoryExtensions' flag.
+8
View File
@@ -17,6 +17,7 @@ import type {
HeapSnapshotDetailedClassDiff,
DuplicateStringGroup,
HeapEdgesQueryOptions,
HeapQueryOptions,
} from './processors/HeapSnapshotManager.js';
import {McpPage} from './McpPage.js';
import {type UncaughtError} from './collectors/PageCollector.js';
@@ -750,6 +751,13 @@ export class McpContext implements Context {
return await this.#heapSnapshotManager.getDuplicateStrings(filePath);
}
async queryHeapSnapshotObjects(
filePath: string,
options: HeapQueryOptions,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
return await this.#heapSnapshotManager.queryObjects(filePath, options);
}
async getHeapSnapshotStats(
filePath: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics> {
+82
View File
@@ -1100,6 +1100,88 @@ export const commands: Commands = {
},
},
},
query_heapsnapshot_objects: {
description:
'Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, minRetainedSize, maxRetainedSize, minSelfSize, isDetached, sortBy). (requires flag: --memoryDebugging=true)',
category: 'Memory',
args: {
filePath: {
name: 'filePath',
type: 'string',
description: 'A path to a .heapsnapshot file to read.',
required: true,
},
className: {
name: 'className',
type: 'string',
description: 'Optional regex or text matching object class name.',
required: false,
},
propertyName: {
name: 'propertyName',
type: 'string',
description:
'Optional property name filter for outgoing reference edges.',
required: false,
},
nodeType: {
name: 'nodeType',
type: 'string',
description:
'Optional V8 node type filter (e.g. object, closure, string, array, code).',
required: false,
},
minRetainedSize: {
name: 'minRetainedSize',
type: 'number',
description: 'Minimum retained size in bytes.',
required: false,
},
maxRetainedSize: {
name: 'maxRetainedSize',
type: 'number',
description: 'Maximum retained size in bytes.',
required: false,
},
minSelfSize: {
name: 'minSelfSize',
type: 'number',
description: 'Minimum self size in bytes.',
required: false,
},
maxSelfSize: {
name: 'maxSelfSize',
type: 'number',
description: 'Maximum self size in bytes.',
required: false,
},
isDetached: {
name: 'isDetached',
type: 'boolean',
description: 'Whether to filter for detached DOM nodes.',
required: false,
},
sortBy: {
name: 'sortBy',
type: 'string',
description: 'Sort order for results. Default is retainedSize.',
required: false,
enum: ['retainedSize', 'selfSize', 'id'],
},
pageIdx: {
name: 'pageIdx',
type: 'number',
description: 'The page index for pagination.',
required: false,
},
pageSize: {
name: 'pageSize',
type: 'number',
description: 'The page size for pagination.',
required: false,
},
},
},
reload_extension: {
description:
'Reloads an unpacked Chrome extension by its ID. (requires flag: --categoryExtensions=true)',
+9 -1
View File
@@ -48,7 +48,6 @@ export type HeapQueryOptions =
export type HeapEdgesQueryOptions =
DevTools.HeapSnapshotModel.HeapSnapshotModel.HeapEdgesQueryOptions;
const VALID_EXTENSIONS: readonly string[] = ['.heapsnapshot', '.heaptimeline'];
function hasValidHeapSnapshotExtension(filePath: string): boolean {
@@ -423,6 +422,15 @@ export class HeapSnapshotManager {
return await snapshot.getDuplicateStrings();
}
async queryObjects(
filePath: string,
options: HeapQueryOptions,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
const snapshot = await this.getSnapshot(filePath);
const provider = snapshot.queryObjects(options);
return await provider.serializeItemsRange(0, Infinity);
}
hasSnapshots(): boolean {
return this.#snapshots.size > 0;
}
+53
View File
@@ -978,5 +978,58 @@
"argType": "number"
}
]
},
{
"name": "query_heapsnapshot_objects",
"args": [
{
"name": "file_path_length",
"argType": "number"
},
{
"name": "class_name_length",
"argType": "number"
},
{
"name": "property_name_length",
"argType": "number"
},
{
"name": "node_type_length",
"argType": "number"
},
{
"name": "min_retained_size",
"argType": "number"
},
{
"name": "max_retained_size",
"argType": "number"
},
{
"name": "min_self_size",
"argType": "number"
},
{
"name": "max_self_size",
"argType": "number"
},
{
"name": "is_detached",
"argType": "boolean"
},
{
"name": "sort_by",
"argType": "string"
},
{
"name": "page_idx",
"argType": "number"
},
{
"name": "page_size",
"argType": "number"
}
]
}
]
+5
View File
@@ -11,6 +11,7 @@ import type {
HeapSnapshotDetailedClassDiff,
DuplicateStringGroup,
HeapEdgesQueryOptions,
HeapQueryOptions,
} from '../processors/HeapSnapshotManager.js';
import type {McpPage} from '../McpPage.js';
import {zod} from '../third_party/index.js';
@@ -315,6 +316,10 @@ export type Context = Readonly<{
currentFilePath: string,
classIndex: number,
): Promise<HeapSnapshotDetailedClassDiff>;
queryHeapSnapshotObjects(
filePath: string,
options: HeapQueryOptions,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
}>;
/**
+77
View File
@@ -465,3 +465,80 @@ export const getHeapSnapshotObjectDetails = defineTool({
response.setHeapSnapshotObjectDetails(objectInfo);
},
});
export const queryHeapSnapshotObjects = defineTool({
name: 'query_heapsnapshot_objects',
description:
'Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, minRetainedSize, maxRetainedSize, minSelfSize, isDetached, sortBy).',
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: true,
conditions: ['memoryDebugging'],
},
blockedByDialog: false,
verifyFilesSchema: {filePath: true},
schema: {
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
className: zod
.string()
.optional()
.describe('Optional regex or text matching object class name.'),
propertyName: zod
.string()
.optional()
.describe('Optional property name filter for outgoing reference edges.'),
nodeType: zod
.string()
.optional()
.describe(
'Optional V8 node type filter (e.g. object, closure, string, array, code).',
),
minRetainedSize: zod
.number()
.optional()
.describe('Minimum retained size in bytes.'),
maxRetainedSize: zod
.number()
.optional()
.describe('Maximum retained size in bytes.'),
minSelfSize: zod
.number()
.optional()
.describe('Minimum self size in bytes.'),
maxSelfSize: zod
.number()
.optional()
.describe('Maximum self size in bytes.'),
isDetached: zod
.boolean()
.optional()
.describe('Whether to filter for detached DOM nodes.'),
sortBy: zod
.enum(['retainedSize', 'selfSize', 'id'])
.optional()
.describe('Sort order for results. Default is retainedSize.'),
pageIdx: zod.number().optional().describe('The page index for pagination.'),
pageSize: zod.number().optional().describe('The page size for pagination.'),
},
handler: async (request, response, context) => {
const range = await context.queryHeapSnapshotObjects(
request.params.filePath,
{
className: request.params.className,
propertyName: request.params.propertyName,
nodeType: request.params.nodeType,
minRetainedSize: request.params.minRetainedSize,
maxRetainedSize: request.params.maxRetainedSize,
minSelfSize: request.params.minSelfSize,
maxSelfSize: request.params.maxSelfSize,
isDetached: request.params.isDetached,
sortBy: request.params.sortBy,
},
);
response.setHeapSnapshotNodes(range, {
pageIdx: request.params.pageIdx,
pageSize: request.params.pageSize,
});
},
});
+63
View File
@@ -449,3 +449,66 @@ Retained by context size: 3.5 kB (148 objects)
Not retained by context size: 798 kB (11792 objects)
Total size: 802 kB
`;
exports[`memory > query_heapsnapshot_objects > with className filter 1`] = `
## Heap Snapshot Data
nodeId,nodeName,type,distance,selfSize,retainedSize
27635,Window (global*) / https://example.com,object,2,40.5 kB,51.0 kB
30887,Window (global*) / https://example.com,object,2,40.5 kB,50.9 kB
30967,Window (prototype) / https://example.com,object,4,26.6 kB,26.7 kB
42085,Window (prototype) / https://example.com,object,4,26.6 kB,26.7 kB
32995,Window (internal cache) / https://example.com,object,3,0.2 kB,10.1 kB
41995,Window (internal cache) / https://example.com,object,3,0.2 kB,10.1 kB
16321,Window / https://example.com,native,2,0.9 kB,1.9 kB
16329,Window,closure,3,0.1 kB,0.4 kB
16341,Window,closure,3,0.1 kB,0.4 kB
30975,Window (prototype) / https://example.com,object,3,0.0 kB,0.2 kB
Showing 1-10 of 34 (Page 1 of 4).
Next page: 1
`;
exports[`memory > query_heapsnapshot_objects > with default options 1`] = `
## Heap Snapshot Data
nodeId,nodeName,type,distance,selfSize,retainedSize
1,,synthetic,100000000,0.0 kB,802 kB
7249,system / NativeContext / https://example.com,hidden,1,1.2 kB,350 kB
7199,system / NativeContext,hidden,1,1.2 kB,84.4 kB
7307,system / NativeContext,hidden,1,1.2 kB,84.4 kB
7195,system / NativeContext / https://example.com,hidden,1,1.2 kB,60.1 kB
27635,Window (global*) / https://example.com,object,2,40.5 kB,51.0 kB
30887,Window (global*) / https://example.com,object,2,40.5 kB,50.9 kB
3,(GC roots),synthetic,100000001,0.0 kB,45.6 kB
49547,,array,2,4.1 kB,41.0 kB
36241,,array,2,4.1 kB,41.0 kB
Showing 1-10 of 27466 (Page 1 of 2747).
Next page: 1
`;
exports[`memory > query_heapsnapshot_objects > with minRetainedSize filter 1`] = `
## Heap Snapshot Data
nodeId,nodeName,type,distance,selfSize,retainedSize
1,,synthetic,100000000,0.0 kB,802 kB
7249,system / NativeContext / https://example.com,hidden,1,1.2 kB,350 kB
7199,system / NativeContext,hidden,1,1.2 kB,84.4 kB
7307,system / NativeContext,hidden,1,1.2 kB,84.4 kB
7195,system / NativeContext / https://example.com,hidden,1,1.2 kB,60.1 kB
27635,Window (global*) / https://example.com,object,2,40.5 kB,51.0 kB
30887,Window (global*) / https://example.com,object,2,40.5 kB,50.9 kB
3,(GC roots),synthetic,100000001,0.0 kB,45.6 kB
49547,,array,2,4.1 kB,41.0 kB
36241,,array,2,4.1 kB,41.0 kB
Showing 1-10 of 148 (Page 1 of 15).
Next page: 1
`;
exports[`memory > query_heapsnapshot_objects > with sortBy selfSize and pagination 1`] = `
## Heap Snapshot Data
nodeId,nodeName,type,distance,selfSize,retainedSize
1,,synthetic,100000000,0.0 kB,802 kB
7249,system / NativeContext / https://example.com,hidden,1,1.2 kB,350 kB
7199,system / NativeContext,hidden,1,1.2 kB,84.4 kB
7307,system / NativeContext,hidden,1,1.2 kB,84.4 kB
7195,system / NativeContext / https://example.com,hidden,1,1.2 kB,60.1 kB
Showing 1-5 of 27466 (Page 1 of 5494).
Next page: 1
`;
+91
View File
@@ -24,6 +24,7 @@ import {
compareHeapSnapshots,
getHeapSnapshotDuplicateStrings,
getHeapSnapshotObjectDetails,
queryHeapSnapshotObjects,
} from '../../src/tools/memory.js';
import {stableIdSymbol} from '../../src/utils/id.js';
import {resolveCanonicalPath} from '../../src/utils/files.js';
@@ -650,4 +651,94 @@ describe('memory', () => {
});
});
});
describe('query_heapsnapshot_objects', () => {
it('with default options', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await queryHeapSnapshotObjects.handler(
{params: {filePath, pageSize: 10}},
response,
context,
);
const responseData = await response.handle(context);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
it('with className filter', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await queryHeapSnapshotObjects.handler(
{params: {filePath, className: 'Window', pageSize: 10}},
response,
context,
);
const responseData = await response.handle(context);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
it('with minRetainedSize filter', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await queryHeapSnapshotObjects.handler(
{params: {filePath, minRetainedSize: 1000, pageSize: 10}},
response,
context,
);
const responseData = await response.handle(context);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
it('with sortBy selfSize and pagination', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await queryHeapSnapshotObjects.handler(
{params: {filePath, sortBy: 'selfSize', pageSize: 5, pageIdx: 0}},
response,
context,
);
const responseData = await response.handle(context);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
});
});