feat: Add the get_heapsnapshot_edges MCP tool (#2180)

This PR adds the get_heapsnapshot_edges MCP tool. Agents can use it to
look at the outgoing edges for a specifc object.

Co-authored-by: Dominik Inführ <dinfuehr@chromium.org>
This commit is contained in:
Dominik Inführ
2026-06-09 21:58:43 +02:00
committed by GitHub
parent 6bd8c91678
commit 4f8eb7ad6b
10 changed files with 247 additions and 2 deletions
+2 -1
View File
@@ -514,11 +514,12 @@ 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** (7 tools)
- **Memory** (8 tools)
- [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
- [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
- [`get_heapsnapshot_class_nodes`](docs/tool-reference.md#get_heapsnapshot_class_nodes)
- [`get_heapsnapshot_details`](docs/tool-reference.md#get_heapsnapshot_details)
- [`get_heapsnapshot_edges`](docs/tool-reference.md#get_heapsnapshot_edges)
- [`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)
+15 -1
View File
@@ -39,11 +39,12 @@
- [`take_snapshot`](#take_snapshot)
- [`screencast_start`](#screencast_start)
- [`screencast_stop`](#screencast_stop)
- **[Memory](#memory)** (7 tools)
- **[Memory](#memory)** (8 tools)
- [`take_heapsnapshot`](#take_heapsnapshot)
- [`close_heapsnapshot`](#close_heapsnapshot)
- [`get_heapsnapshot_class_nodes`](#get_heapsnapshot_class_nodes)
- [`get_heapsnapshot_details`](#get_heapsnapshot_details)
- [`get_heapsnapshot_edges`](#get_heapsnapshot_edges)
- [`get_heapsnapshot_retainers`](#get_heapsnapshot_retainers)
- [`get_heapsnapshot_retaining_paths`](#get_heapsnapshot_retaining_paths)
- [`get_heapsnapshot_summary`](#get_heapsnapshot_summary)
@@ -492,6 +493,19 @@ in the DevTools Elements panel (if any).
---
### `get_heapsnapshot_edges`
**Description:** Loads a memory heapsnapshot and returns outgoing edges (references) for a specific node ID. (requires flag: --memoryDebugging=true)
**Parameters:**
- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
- **nodeId** (number) **(required)**: The node ID to get outgoing edges for.
- **pageIdx** (number) _(optional)_: The page index for pagination.
- **pageSize** (number) _(optional)_: The page size for pagination.
---
### `get_heapsnapshot_retainers`
**Description:** Loads a memory heapsnapshot and returns retainers for a specific node ID. (requires flag: --memoryDebugging=true)
+13
View File
@@ -148,6 +148,19 @@ export class HeapSnapshotManager {
);
}
async getEdges(
filePath: string,
nodeId: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
const snapshot = await this.getSnapshot(filePath);
const nodeIndex = await snapshot.nodeIndexForId(nodeId);
if (nodeIndex === undefined) {
throw new Error(`Node with ID ${nodeId} not found`);
}
const provider = snapshot.createEdgesProvider(nodeIndex);
return await provider.serializeItemsRange(0, Infinity);
}
#getCachedSnapshot(filePath: string) {
const absolutePath = path.resolve(filePath);
const cached = this.#snapshots.get(absolutePath);
+7
View File
@@ -968,4 +968,11 @@ export class McpContext implements Context {
throw new Error(`Unsupported protocol for: ${url}`);
}
}
async getHeapSnapshotEdges(
filePath: string,
nodeId: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
return await this.#heapSnapshotManager.getEdges(filePath, nodeId);
}
}
+31
View File
@@ -360,6 +360,37 @@ export const commands: Commands = {
},
},
},
get_heapsnapshot_edges: {
description:
'Loads a memory heapsnapshot and returns outgoing edges (references) for a specific node ID. (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 outgoing edges for.',
required: true,
},
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,
},
},
},
get_heapsnapshot_retainers: {
description:
'Loads a memory heapsnapshot and returns retainers for a specific node ID. (requires flag: --memoryDebugging=true)',
+21
View File
@@ -776,5 +776,26 @@
"argType": "number"
}
]
},
{
"name": "get_heapsnapshot_edges",
"args": [
{
"name": "file_path_length",
"argType": "number"
},
{
"name": "node_id",
"argType": "number"
},
{
"name": "page_idx",
"argType": "number"
},
{
"name": "page_size",
"argType": "number"
}
]
}
]
+4
View File
@@ -260,6 +260,10 @@ export type Context = Readonly<{
maxNodes?: number,
maxSiblings?: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths>;
getHeapSnapshotEdges(
filePath: string,
nodeId: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
}>;
/**
+30
View File
@@ -223,3 +223,33 @@ export const getHeapSnapshotRetainingPaths = defineTool({
response.setHeapSnapshotRetainingPaths(retainingPaths);
},
});
export const getHeapSnapshotEdges = defineTool({
name: 'get_heapsnapshot_edges',
description:
'Loads a memory heapsnapshot and returns outgoing edges (references) for a specific node ID.',
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 outgoing edges for.'),
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 edges = await context.getHeapSnapshotEdges(
request.params.filePath,
request.params.nodeId,
);
response.setHeapSnapshotNodes(edges, {
pageIdx: request.params.pageIdx,
pageSize: request.params.pageSize,
});
},
});
+71
View File
@@ -175,6 +175,77 @@ id,name,count,selfSize,maxRetainedSize
108,HTMLBodyElement (internal cache) / https://example.com,1,0.0 kB,0.0 kB
`;
exports[`memory > get_heapsnapshot_edges > with pagination 1`] = `
## Heap Snapshot Data
name,type,nodeId,nodeName
constructor,property,25575,String
anchor,property,29295,anchor
Showing 1-2 of 56 (Page 1 of 28).
Next page: 1
`;
exports[`memory > get_heapsnapshot_edges > with valid nodeId 1`] = `
## Heap Snapshot Data
name,type,nodeId,nodeName
constructor,property,25575,String
anchor,property,29295,anchor
at,property,29297,at
big,property,29299,big
blink,property,29301,blink
bold,property,29303,bold
charAt,property,29305,charAt
charCodeAt,property,29307,charCodeAt
codePointAt,property,29309,codePointAt
concat,property,29311,concat
endsWith,property,29313,endsWith
fontcolor,property,29315,fontcolor
fontsize,property,29317,fontsize
fixed,property,29319,fixed
includes,property,29321,includes
indexOf,property,29323,indexOf
isWellFormed,property,29325,isWellFormed
italics,property,29327,italics
lastIndexOf,property,29329,lastIndexOf
link,property,29331,link
localeCompare,property,29333,localeCompare
match,property,29335,match
matchAll,property,29337,matchAll
normalize,property,29339,normalize
padEnd,property,29341,padEnd
padStart,property,29343,padStart
repeat,property,29345,repeat
replace,property,29347,replace
replaceAll,property,29349,replaceAll
search,property,29351,search
slice,property,29353,slice
small,property,29355,small
split,property,29357,split
strike,property,29359,strike
sub,property,29361,sub
substr,property,29363,substr
substring,property,29365,substring
sup,property,29367,sup
startsWith,property,29369,startsWith
toString,property,29371,toString
toWellFormed,property,29373,toWellFormed
trim,property,29375,trim
trimStart,property,29377,trimStart
trimLeft,property,29377,trimStart
trimEnd,property,29379,trimEnd
trimRight,property,29379,trimEnd
toLocaleLowerCase,property,29381,toLocaleLowerCase
toLocaleUpperCase,property,29383,toLocaleUpperCase
toLowerCase,property,29385,toLowerCase
toUpperCase,property,29387,toUpperCase
valueOf,property,29389,valueOf
<symbol Symbol.iterator>,property,29391,[Symbol.iterator]
__proto__,property,25329,{constructor, __defineGetter__, __defineSetter__, …, get __proto__, set __proto__, toLocaleString}
properties,internal,29393,system / PropertyArray
map,internal,25577,system / Map
1,hidden,71,
Showing 1-56 of 56 (Page 1 of 1).
`;
exports[`memory > get_heapsnapshot_retainers > with valid nodeId 1`] = `
## Heap Snapshot Data
name,type,nodeId,nodeName
+53
View File
@@ -19,6 +19,7 @@ import {
getHeapSnapshotRetainers,
closeHeapSnapshot,
getHeapSnapshotRetainingPaths,
getHeapSnapshotEdges,
} from '../../src/tools/memory.js';
import {withMcpContext} from '../utils.js';
@@ -280,4 +281,56 @@ describe('memory', () => {
});
});
});
describe('get_heapsnapshot_edges', () => {
it('with valid nodeId', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await getHeapSnapshotEdges.handler(
{params: {filePath, nodeId: 25341}},
response,
context,
);
const responseData = await response.handle(
getHeapSnapshotEdges.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
it('with pagination', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await getHeapSnapshotEdges.handler(
{params: {filePath, nodeId: 25341, pageSize: 2}},
response,
context,
);
const responseData = await response.handle(
getHeapSnapshotEdges.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
});
});