feat: Add get_heapsnapshot_object_details MCP tool (#2374)

Add a get_heapsnapshot_object_details MCP tool which lets the agent
query all known information about a node in the heap snapshot.

Co-authored-by: Dominik Inführ <dinfuehr@chromium.org>
This commit is contained in:
Dominik Inführ
2026-07-16 13:23:43 +02:00
committed by GitHub
parent f78a911dc0
commit 8432cb97a2
12 changed files with 193 additions and 2 deletions
+2 -1
View File
@@ -524,7 +524,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** (11 tools)
- **Memory** (12 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)
@@ -533,6 +533,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- [`get_heapsnapshot_dominators`](docs/tool-reference.md#get_heapsnapshot_dominators)
- [`get_heapsnapshot_duplicate_strings`](docs/tool-reference.md#get_heapsnapshot_duplicate_strings)
- [`get_heapsnapshot_edges`](docs/tool-reference.md#get_heapsnapshot_edges)
- [`get_heapsnapshot_object_details`](docs/tool-reference.md#get_heapsnapshot_object_details)
- [`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)
+13 -1
View File
@@ -39,7 +39,7 @@
- [`take_snapshot`](#take_snapshot)
- [`screencast_start`](#screencast_start)
- [`screencast_stop`](#screencast_stop)
- **[Memory](#memory)** (11 tools)
- **[Memory](#memory)** (12 tools)
- [`take_heapsnapshot`](#take_heapsnapshot)
- [`close_heapsnapshot`](#close_heapsnapshot)
- [`compare_heapsnapshots`](#compare_heapsnapshots)
@@ -48,6 +48,7 @@
- [`get_heapsnapshot_dominators`](#get_heapsnapshot_dominators)
- [`get_heapsnapshot_duplicate_strings`](#get_heapsnapshot_duplicate_strings)
- [`get_heapsnapshot_edges`](#get_heapsnapshot_edges)
- [`get_heapsnapshot_object_details`](#get_heapsnapshot_object_details)
- [`get_heapsnapshot_retainers`](#get_heapsnapshot_retainers)
- [`get_heapsnapshot_retaining_paths`](#get_heapsnapshot_retaining_paths)
- [`get_heapsnapshot_summary`](#get_heapsnapshot_summary)
@@ -539,6 +540,17 @@ in the DevTools Elements panel (if any).
---
### `get_heapsnapshot_object_details`
**Description:** Loads a memory heapsnapshot and returns detailed information about a specific object by its node ID, including size, type, distance, and DOM detachedness. (requires flag: --memoryDebugging=true)
**Parameters:**
- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
- **nodeId** (number) **(required)**: The node ID to get object details for.
---
### `get_heapsnapshot_retainers`
**Description:** Loads a memory heapsnapshot and returns retainers for a specific node ID. (requires flag: --memoryDebugging=true)
+12
View File
@@ -174,6 +174,18 @@ export class HeapSnapshotManager {
return await provider.serializeItemsRange(0, Infinity);
}
async getObjectInfo(
filePath: string,
nodeId: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo> {
const snapshot = await this.getSnapshot(filePath);
const nodeIndex = await snapshot.nodeIndexForId(nodeId);
if (nodeIndex === undefined) {
throw new Error(`Node with ID ${nodeId} not found`);
}
return await snapshot.getObjectInfo(nodeIndex);
}
async getRetainingPaths(
filePath: string,
nodeId: number,
+7
View File
@@ -693,6 +693,13 @@ export class McpContext implements Context {
return await this.#heapSnapshotManager.getRetainers(filePath, nodeId);
}
async getHeapSnapshotObjectDetails(
filePath: string,
nodeId: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo> {
return await this.#heapSnapshotManager.getObjectInfo(filePath, nodeId);
}
async closeHeapSnapshot(filePath: string): Promise<boolean> {
return this.#heapSnapshotManager.dispose(filePath);
}
+22
View File
@@ -90,6 +90,7 @@ export class McpResponse implements Response {
classDiffs?: HeapSnapshotClassDiff[];
detailedClassDiff?: HeapSnapshotDetailedClassDiff;
duplicateStrings?: DuplicateStringGroup[];
objectInfo?: DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo;
};
#networkRequestsOptions?: {
include: boolean;
@@ -404,6 +405,16 @@ export class McpResponse implements Response {
};
}
setHeapSnapshotObjectDetails(
objectInfo: DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo,
) {
this.#heapSnapshotOptions = {
...this.#heapSnapshotOptions,
include: true,
objectInfo,
};
}
attachImage(value: ImageContentData): void {
this.#images.push(value);
}
@@ -746,6 +757,7 @@ export class McpResponse implements Response {
heapSnapshotClassDiffs?: HeapSnapshotClassDiff[];
heapSnapshotDetailedClassDiff?: HeapSnapshotDetailedClassDiff;
heapSnapshotDuplicateStrings?: readonly DuplicateStringGroup[];
heapSnapshotObjectDetails?: DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo;
extensionServiceWorkers?: object[];
extensionPages?: object[];
errorMessage?: string;
@@ -1177,6 +1189,16 @@ Call ${handleDialog.name} to handle it before continuing.`);
structuredContent.heapSnapshotDuplicateStrings = paginationData.items;
}
const objectInfo = this.#heapSnapshotOptions.objectInfo;
if (objectInfo) {
response.push('### Object Details');
response.push(
compactEncode
? compactEncode(objectInfo)
: HeapSnapshotFormatter.formatObjectInfo(objectInfo),
);
structuredContent.heapSnapshotObjectDetails = objectInfo;
}
}
if (data.detailedNetworkRequest) {
+19
View File
@@ -487,6 +487,25 @@ export const commands: Commands = {
},
},
},
get_heapsnapshot_object_details: {
description:
'Loads a memory heapsnapshot and returns detailed information about a specific object by its node ID, including size, type, distance, and DOM detachedness. (requires flag: --memoryDebugging=true)',
category: 'Memory',
args: {
filePath: {
name: 'filePath',
type: 'string',
description: 'A path to a .heapsnapshot file to read.',
required: true,
},
nodeId: {
name: 'nodeId',
type: 'number',
description: 'The node ID to get object details for.',
required: true,
},
},
},
get_heapsnapshot_retainers: {
description:
'Loads a memory heapsnapshot and returns retainers for a specific node ID. (requires flag: --memoryDebugging=true)',
+31
View File
@@ -243,6 +243,37 @@ export class HeapSnapshotFormatter {
return lines.join('\n');
}
static formatObjectInfo(
info: DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo,
): string {
const lines = [
`id: @${info.id}`,
`name: ${info.name}`,
`type: ${info.type}`,
`detachedness: ${formatDOMLinkState(info.detachedness)}`,
`selfSize: ${DevTools.I18n.ByteUtilities.formatBytesToKb(info.selfSize)}`,
`retainedSize: ${DevTools.I18n.ByteUtilities.formatBytesToKb(info.retainedSize)}`,
`distance: ${info.distance}`,
`edgeCount: ${info.edgeCount}`,
`retainerCount: ${info.retainerCount}`,
];
return lines.join('\n');
}
}
function formatDOMLinkState(
state: DevTools.HeapSnapshotModel.HeapSnapshotModel.DOMLinkState,
): string {
switch (state) {
case DevTools.HeapSnapshotModel.HeapSnapshotModel.DOMLinkState.ATTACHED:
return 'attached';
case DevTools.HeapSnapshotModel.HeapSnapshotModel.DOMLinkState.DETACHED:
return 'detached';
case DevTools.HeapSnapshotModel.HeapSnapshotModel.DOMLinkState.UNKNOWN:
default:
return 'unknown';
}
}
function formatSignedCount(n: number): string {
+13
View File
@@ -884,5 +884,18 @@
"argType": "number"
}
]
},
{
"name": "get_heapsnapshot_object_details",
"args": [
{
"name": "file_path_length",
"argType": "number"
},
{
"name": "node_id",
"argType": "number"
}
]
}
]
+7
View File
@@ -134,6 +134,9 @@ export interface Response {
setHeapSnapshotDetailedClassDiff(
detailedClassDiff: HeapSnapshotDetailedClassDiff,
): void;
setHeapSnapshotObjectDetails(
objectInfo: DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo,
): void;
setIncludePages(value: boolean): void;
setIncludeNetworkRequests(
value: boolean,
@@ -256,6 +259,10 @@ export type Context = Readonly<{
filePath: string,
nodeId: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
getHeapSnapshotObjectDetails(
filePath: string,
nodeId: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo>;
closeHeapSnapshot(filePath: string): Promise<boolean>;
getHeapSnapshotRetainingPaths(
filePath: string,
+25
View File
@@ -372,3 +372,28 @@ export const getHeapSnapshotDuplicateStrings = defineTool({
});
},
});
export const getHeapSnapshotObjectDetails = defineTool({
name: 'get_heapsnapshot_object_details',
description:
'Loads a memory heapsnapshot and returns detailed information about a specific object by its node ID, including size, type, distance, and DOM detachedness.',
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: true,
conditions: ['memoryDebugging'],
},
blockedByDialog: false,
verifyFilesSchema: ['filePath'],
schema: {
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
nodeId: zod.number().describe('The node ID to get object details for.'),
},
handler: async (request, response, context) => {
const objectInfo = await context.getHeapSnapshotObjectDetails(
request.params.filePath,
request.params.nodeId,
);
response.setHeapSnapshotObjectDetails(objectInfo);
},
});
+14
View File
@@ -332,6 +332,20 @@ map,internal,25577,system / Map
Showing 1-56 of 56 (Page 1 of 1).
`;
exports[`memory > get_heapsnapshot_object_details > with valid nodeId 1`] = `
## Heap Snapshot Data
### Object Details
id: @25341
name: String
type: object
detachedness: unknown
selfSize: 0.2 kB
retainedSize: 1.6 kB
distance: 2
edgeCount: 56
retainerCount: 3
`;
exports[`memory > get_heapsnapshot_retainers > with valid nodeId 1`] = `
## Heap Snapshot Data
name,type,nodeId,nodeName
+28
View File
@@ -23,6 +23,7 @@ import {
getHeapSnapshotDominators,
compareHeapSnapshots,
getHeapSnapshotDuplicateStrings,
getHeapSnapshotObjectDetails,
} from '../../src/tools/memory.js';
import {stableIdSymbol} from '../../src/utils/id.js';
import {withMcpContext} from '../utils.js';
@@ -246,6 +247,33 @@ describe('memory', () => {
});
});
describe('get_heapsnapshot_object_details', () => {
it('with valid nodeId', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await getHeapSnapshotObjectDetails.handler(
{params: {filePath, nodeId: 25341}},
response,
context,
);
const responseData = await response.handle(
getHeapSnapshotObjectDetails.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
});
describe('close_heapsnapshot', () => {
it('with default options', async () => {
await withMcpContext(async (response, context) => {