Skip to content

Commit 8713b93

Browse files
dinfuehrDominik Inführnroscino
authored
feat: Adds close_heapsnapshot MCP tool (ChromeDevTools#2174)
This commit adds the close_heapsnapshot MCP tool such that the coding agent can close heap snapshots again. Co-authored-by: Dominik Inführ <dinfuehr@chromium.org> Co-authored-by: Nicholas Roscino <nroscino@google.com>
1 parent dfc1115 commit 8713b93

9 files changed

Lines changed: 128 additions & 3 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,9 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
514514
- [`take_snapshot`](docs/tool-reference.md#take_snapshot)
515515
- [`screencast_start`](docs/tool-reference.md#screencast_start)
516516
- [`screencast_stop`](docs/tool-reference.md#screencast_stop)
517-
- **Memory** (5 tools)
517+
- **Memory** (6 tools)
518518
- [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
519+
- [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
519520
- [`get_heapsnapshot_class_nodes`](docs/tool-reference.md#get_heapsnapshot_class_nodes)
520521
- [`get_heapsnapshot_details`](docs/tool-reference.md#get_heapsnapshot_details)
521522
- [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)

docs/tool-reference.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@
3939
- [`take_snapshot`](#take_snapshot)
4040
- [`screencast_start`](#screencast_start)
4141
- [`screencast_stop`](#screencast_stop)
42-
- **[Memory](#memory)** (5 tools)
42+
- **[Memory](#memory)** (6 tools)
4343
- [`take_heapsnapshot`](#take_heapsnapshot)
44+
- [`close_heapsnapshot`](#close_heapsnapshot)
4445
- [`get_heapsnapshot_class_nodes`](#get_heapsnapshot_class_nodes)
4546
- [`get_heapsnapshot_details`](#get_heapsnapshot_details)
4647
- [`get_heapsnapshot_retainers`](#get_heapsnapshot_retainers)
@@ -454,6 +455,16 @@ in the DevTools Elements panel (if any).
454455

455456
---
456457

458+
### `close_heapsnapshot`
459+
460+
**Description:** Closes a previously loaded memory heapsnapshot, freeing its memory. (requires flag: --experimentalMemory=true)
461+
462+
**Parameters:**
463+
464+
- **filePath** (string) **(required)**: A path to the .heapsnapshot file to close.
465+
466+
---
467+
457468
### `get_heapsnapshot_class_nodes`
458469

459470
**Description:** Loads a memory heapsnapshot and returns instances of a specific class with their IDs. (requires flag: --experimentalMemory=true)

src/HeapSnapshotManager.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,18 @@ export class HeapSnapshotManager {
201201
return {snapshot, worker: workerProxy};
202202
}
203203

204-
dispose(filePath: string): void {
204+
hasSnapshots(): boolean {
205+
return this.#snapshots.size > 0;
206+
}
207+
208+
dispose(filePath: string): boolean {
205209
const absolutePath = path.resolve(filePath);
206210
const cached = this.#snapshots.get(absolutePath);
207211
if (cached) {
208212
cached.worker.dispose();
209213
this.#snapshots.delete(absolutePath);
214+
return true;
210215
}
216+
return false;
211217
}
212218
}

src/McpContext.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,4 +889,12 @@ export class McpContext implements Context {
889889
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
890890
return await this.#heapSnapshotManager.getRetainers(filePath, nodeId);
891891
}
892+
893+
async closeHeapSnapshot(filePath: string): Promise<boolean> {
894+
return this.#heapSnapshotManager.dispose(filePath);
895+
}
896+
897+
hasHeapSnapshots(): boolean {
898+
return this.#heapSnapshotManager.hasSnapshots();
899+
}
892900
}

src/bin/chrome-devtools-cli-options.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ export const commands: Commands = {
8181
},
8282
},
8383
},
84+
close_heapsnapshot: {
85+
description:
86+
'Closes a previously loaded memory heapsnapshot, freeing its memory. (requires flag: --experimentalMemory=true)',
87+
category: 'Memory',
88+
args: {
89+
filePath: {
90+
name: 'filePath',
91+
type: 'string',
92+
description: 'A path to the .heapsnapshot file to close.',
93+
required: true,
94+
},
95+
},
96+
},
8497
close_page: {
8598
description:
8699
'Closes the page by its index. The last open page cannot be closed.',

src/telemetry/tool_call_metrics.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,5 +738,14 @@
738738
"argType": "number"
739739
}
740740
]
741+
},
742+
{
743+
"name": "close_heapsnapshot",
744+
"args": [
745+
{
746+
"name": "file_path_length",
747+
"argType": "number"
748+
}
749+
]
741750
}
742751
]

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ export type Context = Readonly<{
248248
filePath: string,
249249
nodeId: number,
250250
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
251+
closeHeapSnapshot(filePath: string): Promise<boolean>;
251252
}>;
252253

253254
/**

src/tools/memory.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,32 @@ export const getHeapSnapshotRetainers = defineTool({
154154
});
155155
},
156156
});
157+
158+
export const closeHeapSnapshot = defineTool({
159+
name: 'close_heapsnapshot',
160+
description:
161+
'Closes a previously loaded memory heapsnapshot, freeing its memory.',
162+
annotations: {
163+
category: ToolCategory.MEMORY,
164+
readOnlyHint: false,
165+
conditions: ['experimentalMemory'],
166+
},
167+
verifyFilesSchema: ['filePath'],
168+
schema: {
169+
filePath: zod
170+
.string()
171+
.describe('A path to the .heapsnapshot file to close.'),
172+
},
173+
blockedByDialog: false,
174+
handler: async (request, response, context) => {
175+
const closed = await context.closeHeapSnapshot(request.params.filePath);
176+
if (!closed) {
177+
throw new Error(
178+
`Failed to close heap snapshot: ${request.params.filePath} was not loaded.`,
179+
);
180+
}
181+
response.appendResponseLine(
182+
`Closed heap snapshot: ${request.params.filePath}`,
183+
);
184+
},
185+
});

tests/tools/memory.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
getHeapSnapshotDetails,
1818
getHeapSnapshotClassNodes,
1919
getHeapSnapshotRetainers,
20+
closeHeapSnapshot,
2021
} from '../../src/tools/memory.js';
2122
import {withMcpContext} from '../utils.js';
2223

@@ -176,4 +177,50 @@ describe('memory', () => {
176177
});
177178
});
178179
});
180+
181+
describe('close_heapsnapshot', () => {
182+
it('with default options', async () => {
183+
await withMcpContext(async (response, context) => {
184+
const filePath = join(
185+
process.cwd(),
186+
'tests/fixtures/example.heapsnapshot',
187+
);
188+
189+
await getHeapSnapshotSummary.handler(
190+
{params: {filePath}},
191+
response,
192+
context,
193+
);
194+
195+
assert.ok(context.hasHeapSnapshots());
196+
197+
await closeHeapSnapshot.handler(
198+
{params: {filePath}},
199+
response,
200+
context,
201+
);
202+
203+
assert.ok(
204+
response.responseLines.includes(`Closed heap snapshot: ${filePath}`),
205+
);
206+
assert.ok(!context.hasHeapSnapshots());
207+
});
208+
});
209+
210+
it('with non-existent snapshot', async () => {
211+
await withMcpContext(async (response, context) => {
212+
const filePath = join(
213+
process.cwd(),
214+
'tests/fixtures/example.heapsnapshot',
215+
);
216+
217+
await assert.rejects(
218+
closeHeapSnapshot.handler({params: {filePath}}, response, context),
219+
{
220+
message: `Failed to close heap snapshot: ${filePath} was not loaded.`,
221+
},
222+
);
223+
});
224+
});
225+
});
179226
});

0 commit comments

Comments
 (0)