Skip to content

Commit 0dbfe3d

Browse files
authored
Merge branch 'main' into mcp-dialog-pt1
2 parents d381b8d + 5aa6437 commit 0dbfe3d

17 files changed

Lines changed: 459 additions & 21 deletions

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"files": [
3737
"build/src",
3838
"LICENSE",
39-
"!*.tsbuildinfo"
39+
"!*.tsbuildinfo",
40+
"!*.js.map"
4041
],
4142
"repository": "ChromeDevTools/chrome-devtools-mcp",
4243
"author": "Google LLC",

src/HeapSnapshotManager.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@ export class HeapSnapshotManager {
9999
return uid;
100100
}
101101

102+
async getNodesByUid(
103+
filePath: string,
104+
uid: number,
105+
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
106+
const snapshot = await this.getSnapshot(filePath);
107+
const filter =
108+
new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
109+
const className = await this.resolveClassKeyFromUid(filePath, uid);
110+
if (!className) {
111+
throw new Error(`Class with UID ${uid} not found in heap snapshot`);
112+
}
113+
const provider = snapshot.createNodesProviderForClass(className, filter);
114+
115+
const range = await provider.serializeItemsRange(0, 1);
116+
return await provider.serializeItemsRange(0, range.totalLength);
117+
}
118+
102119
#getCachedSnapshot(filePath: string) {
103120
const absolutePath = path.resolve(filePath);
104121
const cached = this.#snapshots.get(absolutePath);
@@ -108,6 +125,14 @@ export class HeapSnapshotManager {
108125
return cached;
109126
}
110127

128+
async resolveClassKeyFromUid(
129+
filePath: string,
130+
uid: number,
131+
): Promise<string | undefined> {
132+
const cached = this.#getCachedSnapshot(filePath);
133+
return cached.uidToClassKey.get(uid);
134+
}
135+
111136
async #loadSnapshot(absolutePath: string): Promise<{
112137
snapshot: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotProxy;
113138
worker: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy;

src/McpContext.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,4 +765,11 @@ export class McpContext implements Context {
765765
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null> {
766766
return await this.#heapSnapshotManager.getStaticData(filePath);
767767
}
768+
769+
async getHeapSnapshotNodesByUid(
770+
filePath: string,
771+
uid: number,
772+
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
773+
return await this.#heapSnapshotManager.getNodesByUid(filePath, uid);
774+
}
768775
}

src/McpResponse.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ export class McpResponse implements Response {
179179
pagination?: PaginationOptions;
180180
stats?: DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics;
181181
staticData?: DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null;
182+
nodes?: DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange;
182183
};
183184
#networkRequestsOptions?: {
184185
include: boolean;
@@ -404,6 +405,18 @@ export class McpResponse implements Response {
404405
};
405406
}
406407

408+
setHeapSnapshotNodes(
409+
nodes: DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange,
410+
options?: PaginationOptions,
411+
) {
412+
this.#heapSnapshotOptions = {
413+
...this.#heapSnapshotOptions,
414+
include: true,
415+
nodes,
416+
pagination: options,
417+
};
418+
}
419+
407420
attachImage(value: ImageContentData): void {
408421
this.#images.push(value);
409422
}
@@ -701,6 +714,7 @@ export class McpResponse implements Response {
701714
staticData?: object;
702715
};
703716
heapSnapshotData?: object[];
717+
heapSnapshotNodes?: readonly object[];
704718
extensionServiceWorkers?: object[];
705719
extensionPages?: object[];
706720
} = {};
@@ -929,6 +943,20 @@ Call ${handleDialog.name} to handle it before continuing.`);
929943
response.push(formatter.toString());
930944
structuredContent.heapSnapshotData = formatter.toJSON();
931945
}
946+
const nodes = this.#heapSnapshotOptions.nodes;
947+
if (nodes) {
948+
const paginationData = this.#dataWithPagination(
949+
nodes.items,
950+
this.#heapSnapshotOptions.pagination,
951+
);
952+
953+
response.push(HeapSnapshotFormatter.formatNodes(paginationData.items));
954+
955+
structuredContent.pagination = paginationData.pagination;
956+
response.push(...paginationData.info);
957+
958+
structuredContent.heapSnapshotNodes = paginationData.items;
959+
}
932960
}
933961

934962
if (data.detailedNetworkRequest) {
@@ -1031,17 +1059,16 @@ Call ${handleDialog.name} to handle it before continuing.`);
10311059

10321060
response.push('## Console messages');
10331061
if (messages.length) {
1062+
const grouped = ConsoleFormatter.groupConsecutive(messages);
10341063
const paginationData = this.#dataWithPagination(
1035-
messages,
1064+
grouped,
10361065
this.#consoleDataOptions.pagination,
10371066
);
10381067
structuredContent.pagination = paginationData.pagination;
10391068
response.push(...paginationData.info);
1040-
response.push(
1041-
...paginationData.items.map(message => message.toString()),
1042-
);
1043-
structuredContent.consoleMessages = paginationData.items.map(message =>
1044-
message.toJSON(),
1069+
response.push(...paginationData.items.map(item => item.toString()));
1070+
structuredContent.consoleMessages = paginationData.items.map(item =>
1071+
item.toJSON(),
10451072
);
10461073
} else {
10471074
response.push('<no console messages found>');

src/TextSnapshot.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,12 @@ export class TextSnapshot {
231231
};
232232

233233
const findDescendantNodes = async (
234-
backendNodeId: number,
234+
backendNodeId?: number,
235235
): Promise<Set<number>> => {
236236
const descendantIds = new Set<number>();
237+
if (!backendNodeId) {
238+
return descendantIds;
239+
}
237240
try {
238241
// @ts-expect-error internal API
239242
const client = page.pptrPage._client();
@@ -297,20 +300,26 @@ export class TextSnapshot {
297300
if (extraHandles.length) {
298301
page.extraHandles = extraHandles;
299302
}
303+
const reorgInfo: Array<{
304+
extraNode: TextSnapshotNode;
305+
attachTarget: TextSnapshotNode;
306+
descendantIds: Set<number>;
307+
}> = [];
308+
300309
for (const handle of page.extraHandles) {
301310
const extraNode = await createExtraNode(handle);
302311
if (!extraNode) {
303312
continue;
304313
}
305314
idToNode.set(extraNode.id, extraNode);
306315
const attachTarget = (await findAncestorNode(handle)) || rootNodeWithId;
307-
if (extraNode.backendNodeId !== undefined) {
308-
const descendantIds = await findDescendantNodes(
309-
extraNode.backendNodeId,
310-
);
311-
const index = moveChildNodes(attachTarget, extraNode, descendantIds);
312-
attachTarget.children.splice(index, 0, extraNode);
313-
}
316+
const descendantIds = await findDescendantNodes(extraNode.backendNodeId);
317+
reorgInfo.push({extraNode, attachTarget, descendantIds});
318+
}
319+
320+
for (const {extraNode, attachTarget, descendantIds} of reorgInfo) {
321+
const index = moveChildNodes(attachTarget, extraNode, descendantIds);
322+
attachTarget.children.splice(index, 0, extraNode);
314323
}
315324
}
316325
}

src/formatters/ConsoleFormatter.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {UncaughtError} from '../PageCollector.js';
1313
import * as DevTools from '../third_party/index.js';
1414
import type {ConsoleMessage} from '../third_party/index.js';
1515

16+
import type {IssueFormatter} from './IssueFormatter.js';
17+
1618
export interface ConsoleFormatterOptions {
1719
fetchDetailedData?: boolean;
1820
id: number;
@@ -32,6 +34,7 @@ interface ConsoleMessageConcise {
3234
text: string;
3335
argsCount: number;
3436
id: number;
37+
count?: number;
3538
}
3639

3740
interface ConsoleMessageDetailed extends ConsoleMessageConcise {
@@ -54,7 +57,7 @@ export class ConsoleFormatter {
5457

5558
readonly isIgnored: IgnoreCheck;
5659

57-
private constructor(params: {
60+
protected constructor(params: {
5861
id: number;
5962
type: string;
6063
text: string;
@@ -201,6 +204,48 @@ export class ConsoleFormatter {
201204
};
202205
}
203206

207+
/**
208+
* Groups consecutive messages with the same type, text, and argument count.
209+
* Similar to Chrome DevTools' console grouping behavior.
210+
*/
211+
static groupConsecutive(
212+
messages: Array<ConsoleFormatter | IssueFormatter>,
213+
): Array<ConsoleFormatter | IssueFormatter> {
214+
const grouped: Array<{
215+
message: ConsoleFormatter | IssueFormatter;
216+
count: number;
217+
}> = [];
218+
for (const msg of messages) {
219+
const prev = grouped[grouped.length - 1];
220+
if (
221+
prev &&
222+
prev.message instanceof ConsoleFormatter &&
223+
msg instanceof ConsoleFormatter &&
224+
prev.message.#type === msg.#type &&
225+
prev.message.#text === msg.#text &&
226+
prev.message.#argCount === msg.#argCount
227+
) {
228+
prev.count++;
229+
} else {
230+
grouped.push({message: msg, count: 1});
231+
}
232+
}
233+
return grouped.map(({message, count}) =>
234+
count > 1 && message instanceof ConsoleFormatter
235+
? new GroupedConsoleFormatter(
236+
{
237+
id: message.#id,
238+
type: message.#type,
239+
text: message.#text,
240+
argCount: message.#argCount,
241+
isIgnored: message.isIgnored,
242+
},
243+
count,
244+
)
245+
: message,
246+
);
247+
}
248+
204249
toJSONDetailed(): ConsoleMessageDetailed {
205250
return {
206251
id: this.#id,
@@ -215,8 +260,37 @@ export class ConsoleFormatter {
215260
}
216261
}
217262

263+
export class GroupedConsoleFormatter extends ConsoleFormatter {
264+
readonly #count: number;
265+
266+
constructor(
267+
params: {
268+
id: number;
269+
type: string;
270+
text: string;
271+
argCount: number;
272+
isIgnored: IgnoreCheck;
273+
},
274+
count: number,
275+
) {
276+
super(params);
277+
this.#count = count;
278+
}
279+
280+
override toString(): string {
281+
return convertConsoleMessageConciseToString(this.toJSON());
282+
}
283+
284+
override toJSON(): ConsoleMessageConcise {
285+
const json = super.toJSON();
286+
json.count = this.#count;
287+
return json;
288+
}
289+
}
290+
218291
function convertConsoleMessageConciseToString(msg: ConsoleMessageConcise) {
219-
return `msgid=${msg.id} [${msg.type}] ${msg.text} (${msg.argsCount} args)`;
292+
const countSuffix = msg.count && msg.count > 1 ? ` [${msg.count} times]` : '';
293+
return `msgid=${msg.id} [${msg.type}] ${msg.text} (${msg.argsCount} args)${countSuffix}`;
220294
}
221295

222296
function convertConsoleMessageConciseDetailedToString(

src/formatters/HeapSnapshotFormatter.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,44 @@ export interface FormattedSnapshotEntry {
1616
retainedSize: number;
1717
}
1818

19+
function isNodeLike(
20+
item: unknown,
21+
): item is DevTools.HeapSnapshotModel.HeapSnapshotModel.Node {
22+
return (
23+
typeof item === 'object' && item !== null && 'id' in item && 'name' in item
24+
);
25+
}
26+
1927
export class HeapSnapshotFormatter {
2028
#aggregates: Record<string, AggregatedInfoWithUid>;
2129

2230
constructor(aggregates: Record<string, AggregatedInfoWithUid>) {
2331
this.#aggregates = aggregates;
2432
}
2533

34+
static formatNodes(
35+
items: ReadonlyArray<
36+
| DevTools.HeapSnapshotModel.HeapSnapshotModel.Node
37+
| DevTools.HeapSnapshotModel.HeapSnapshotModel.Edge
38+
>,
39+
): string {
40+
const lines: string[] = [];
41+
42+
if (items.length > 0 && isNodeLike(items[0])) {
43+
lines.push('id,name,type,distance,selfSize,retainedSize');
44+
}
45+
46+
for (const item of items) {
47+
if (isNodeLike(item)) {
48+
lines.push(
49+
`${item.id},"${item.name}",${item.type},${item.distance},${item.selfSize},${item.retainedSize}`,
50+
);
51+
}
52+
}
53+
54+
return lines.join('\n');
55+
}
56+
2657
#getSortedAggregates(): AggregatedInfoWithUid[] {
2758
return Object.values(this.#aggregates).sort((a, b) => b.self - a.self);
2859
}

src/formatters/SnapshotFormatter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class SnapshotFormatter {
2020

2121
// Top-level content of the snapshot.
2222
if (
23-
this.#snapshot.verbose &&
23+
!this.#snapshot.verbose &&
2424
this.#snapshot.hasSelectedElement &&
2525
!this.#snapshot.selectedElementUid
2626
) {

src/telemetry/tool_call_metrics.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,5 +591,22 @@
591591
"argType": "number"
592592
}
593593
]
594+
},
595+
{
596+
"name": "get_nodes_by_class",
597+
"args": [
598+
{
599+
"name": "file_path_length",
600+
"argType": "number"
601+
},
602+
{
603+
"name": "page_idx",
604+
"argType": "number"
605+
},
606+
{
607+
"name": "page_size",
608+
"argType": "number"
609+
}
610+
]
594611
}
595612
]

0 commit comments

Comments
 (0)