mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
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:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user