mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
feat: add experimentalGcfFormat flag for GCF-encoded tool responses (#2235)
## Summary Add `--experimentalGcfFormat` as a hidden experimental flag alongside the existing `--experimentalToonFormat`. When enabled, tool responses encode structured data using GCF (Graph Compact Format) instead of formatted JSON. No changes to existing behavior or default output. ## Why ### Benchmarked on Chrome DevTools data shapes Data shapes verified from source code and test snapshots (`ConsoleMessageConcise`, `NetworkRequestConcise`, `HeapSnapshotFormatter.toJSON()`, `SnapshotFormatter.toJSON()`): | Dataset | JSON | TOON | GCF | TOON vs JSON | GCF vs JSON | GCF vs TOON | |---------|------|------|-----|-------------|-------------|-------------| | Console (100) | 2,534 | 1,716 | 1,663 | 32.3% | 34.4% | +3.1% | | Console (500) | 12,879 | 8,761 | 8,475 | 32.0% | 34.2% | +3.3% | | Network (100) | 3,287 | 1,776 | 1,682 | 46.0% | 48.8% | +5.3% | | Network (500) | 16,547 | 8,936 | 8,442 | 46.0% | 49.0% | +5.5% | | Heap (100) | 2,210 | 797 | 753 | 63.9% | 65.9% | +5.5% | | DOM (3x3) | 282 | 295 | 244 | -4.6% | 13.5% | +17.3% | | DOM (4x3) | 396 | 464 | 407 | -17.2% | -2.8% | +12.3% | TOON increases token count on DOM snapshots (-4.6% to -17.2% vs JSON). GCF saves tokens on every data type. ### Console message corruption TOON's decoder crashes on console messages containing bracket-colon patterns, which are standard browser output: ``` [Error]: net::ERR_CONNECTION_REFUSED [React DevTools]: Component rendered 3 times [Violation]: Forced reflow while executing JavaScript took 42ms [Performance]: Long task detected (duration: 234ms) ``` 10 of 20 console messages fail TOON round-trip. GCF: zero failures. While Chrome DevTools MCP encodes only (no decode), the structurally ambiguous output can affect downstream consumers. ### LLM comprehension GCF scores 100% on general structured data and 90.7% on adversarial payloads across GPT-4o, GPT-5.5, Claude, and Gemini. JSON scores 53.6%. TOON scores 68.5%. (1,700+ evaluations.) Full eval data: [GCF benchmarks](https://gcformat.com/guide/benchmarks) ### Data integrity GCF verified lossless across 43 billion+ round-trips in 5 formats and 6 languages. Zero failures. Zero runtime dependencies. ## Implementation Mirrors the existing `experimentalToonFormat` pattern exactly: - Hidden boolean CLI flag, defaults to false - `useGcf` parameter threaded through `handle()` → `format()` - `compactEncode` helper selects GCF, TOON, or null (formatted JSON) - GCF takes precedence if both flags are set ## Changes | File | Change | |------|--------| | `src/bin/chrome-devtools-mcp-cli-options.ts` | Add `experimentalGcfFormat` flag | | `src/third_party/index.ts` | Export `gcfEncode` from `@blackwell-systems/gcf` | | `src/McpResponse.ts` | Add `useGcf` parameter, `compactEncode` helper | | `src/ToolHandler.ts` | Pass `experimentalGcfFormat` to `handle()` | | `package.json` | Add `@blackwell-systems/gcf` (pinned 2.1.2, zero deps) | ## Links - GCF spec: https://gcformat.com - TypeScript SDK: https://www.npmjs.com/package/@blackwell-systems/gcf - Benchmarks: https://gcformat.com/guide/benchmarks - Lossless verification: https://gcformat.com/guide/lossless-verification --------- Co-authored-by: Piotr Paulski <piotrpaulski@chromium.org>
This commit is contained in:
Generated
+15
@@ -13,6 +13,7 @@
|
||||
"chrome-devtools-mcp": "build/src/bin/chrome-devtools-mcp.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blackwell-systems/gcf": "^2.2.2",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@google/genai": "^2.0.1",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
@@ -53,14 +54,28 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@blackwell-systems/gcf": "^2.2.2",
|
||||
"@toon-format/toon": "^2.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@blackwell-systems/gcf": {
|
||||
"optional": true
|
||||
},
|
||||
"@toon-format/toon": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@blackwell-systems/gcf": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@blackwell-systems/gcf/-/gcf-2.2.2.tgz",
|
||||
"integrity": "sha512-b35EeiMiopwLpgphMVaiVVS5KS88FT/LUt2XsYG5iRKEYl7/p8L23zBMJ2ggh/Ha6USwU92uFu8fTahH0s6q+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"gcf": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
|
||||
|
||||
+6
-1
@@ -55,6 +55,7 @@
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@stylistic/eslint-plugin": "^5.4.0",
|
||||
"@blackwell-systems/gcf": "^2.2.2",
|
||||
"@toon-format/toon": "^2.2.0",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/filesystem": "^0.0.36",
|
||||
@@ -85,11 +86,15 @@
|
||||
"yargs": "18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@toon-format/toon": "^2.2.0"
|
||||
"@toon-format/toon": "^2.2.0",
|
||||
"@blackwell-systems/gcf": "^2.2.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@toon-format/toon": {
|
||||
"optional": true
|
||||
},
|
||||
"@blackwell-systems/gcf": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -284,6 +284,13 @@ export default [
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
source === '@blackwell-systems/gcf' ||
|
||||
source.startsWith('@blackwell-systems/gcf/')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const existingExternals = [
|
||||
'./bidi.js',
|
||||
'../bidi/bidi.js',
|
||||
|
||||
+40
-21
@@ -25,7 +25,12 @@ import type {McpContext} from './McpContext.js';
|
||||
import type {McpPage} from './McpPage.js';
|
||||
import {UncaughtError} from './PageCollector.js';
|
||||
import {TextSnapshot} from './TextSnapshot.js';
|
||||
import {DevTools, getToonEncode, type Protocol} from './third_party/index.js';
|
||||
import {
|
||||
DevTools,
|
||||
getToonEncode,
|
||||
getGcfEncode,
|
||||
type Protocol,
|
||||
} from './third_party/index.js';
|
||||
import type {
|
||||
ConsoleMessage,
|
||||
ImageContent,
|
||||
@@ -50,6 +55,8 @@ import {paginate} from './utils/pagination.js';
|
||||
import type {PaginationOptions} from './utils/types.js';
|
||||
import type {WaitForEventsResult} from './WaitForHelper.js';
|
||||
|
||||
export type DataFormat = 'default' | 'toon' | 'gcf';
|
||||
|
||||
interface TraceInsightData {
|
||||
trace: TraceResult;
|
||||
insightSetId: string;
|
||||
@@ -565,7 +572,7 @@ export class McpResponse implements Response {
|
||||
async handle(
|
||||
toolName: string,
|
||||
context: McpContext,
|
||||
useToon = false,
|
||||
dataFormat: DataFormat = 'default',
|
||||
): Promise<{
|
||||
content: Array<TextContent | ImageContent>;
|
||||
structuredContent: object;
|
||||
@@ -813,7 +820,7 @@ export class McpResponse implements Response {
|
||||
webmcpTools,
|
||||
errorMessage: this.#error?.message,
|
||||
},
|
||||
useToon,
|
||||
dataFormat,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -834,7 +841,7 @@ export class McpResponse implements Response {
|
||||
webmcpTools?: WebMCPTool[];
|
||||
errorMessage?: string;
|
||||
},
|
||||
useToon: boolean,
|
||||
dataFormat: DataFormat = 'default',
|
||||
): Promise<{
|
||||
content: Array<TextContent | ImageContent>;
|
||||
structuredContent: object;
|
||||
@@ -885,18 +892,30 @@ export class McpResponse implements Response {
|
||||
geolocation?: {latitude: number; longitude: number};
|
||||
} = {};
|
||||
|
||||
let toonEncode: ((val: unknown) => string) | undefined;
|
||||
if (useToon) {
|
||||
// Resolve the compact encoder based on the chosen format
|
||||
let compactEncode: ((val: unknown) => string) | undefined;
|
||||
if (dataFormat === 'toon') {
|
||||
try {
|
||||
toonEncode = await getToonEncode();
|
||||
compactEncode = await getToonEncode();
|
||||
} catch {
|
||||
throw new Error(
|
||||
'The `@toon-format/toon` package is required to use the experimental TOON format. ' +
|
||||
'The `@toon-format/toon` package is required to use --experimentalDataFormat=toon. ' +
|
||||
'Make sure the peer dependency is installed:\n' +
|
||||
'- For npx: npx --package chrome-devtools-mcp@latest --package @toon-format/toon@latest chrome-devtools-mcp --experimentalToonFormat\n' +
|
||||
'- For npx: npx --package chrome-devtools-mcp@latest --package @toon-format/toon@latest chrome-devtools-mcp --experimentalDataFormat=toon\n' +
|
||||
'- For npm: npm install @toon-format/toon (add -g if installed globally)',
|
||||
);
|
||||
}
|
||||
} else if (dataFormat === 'gcf') {
|
||||
try {
|
||||
compactEncode = await getGcfEncode();
|
||||
} catch {
|
||||
throw new Error(
|
||||
'The `@blackwell-systems/gcf` package is required to use --experimentalDataFormat=gcf. ' +
|
||||
'Make sure the peer dependency is installed:\n' +
|
||||
'- For npx: npx --package chrome-devtools-mcp@latest --package @blackwell-systems/gcf@latest chrome-devtools-mcp --experimentalDataFormat=gcf\n' +
|
||||
'- For npm: npm install @blackwell-systems/gcf (add -g if installed globally)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const response = [];
|
||||
@@ -1117,8 +1136,8 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
||||
structuredContent.snapshot = data.snapshot.toJSON();
|
||||
response.push('## Latest page snapshot');
|
||||
response.push(
|
||||
useToon && toonEncode
|
||||
? toonEncode(structuredContent.snapshot)
|
||||
compactEncode
|
||||
? compactEncode(structuredContent.snapshot)
|
||||
: data.snapshot.toString(),
|
||||
);
|
||||
}
|
||||
@@ -1155,8 +1174,8 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
||||
|
||||
structuredContent.heapSnapshotData = formatter.toJSON();
|
||||
response.push(
|
||||
useToon && toonEncode
|
||||
? toonEncode(structuredContent.heapSnapshotData)
|
||||
compactEncode
|
||||
? compactEncode(structuredContent.heapSnapshotData)
|
||||
: formatter.toString(),
|
||||
);
|
||||
}
|
||||
@@ -1220,8 +1239,8 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
||||
if (classDiffs) {
|
||||
response.push('### Heap Snapshot Diff');
|
||||
response.push(
|
||||
useToon && toonEncode
|
||||
? toonEncode(classDiffs)
|
||||
compactEncode
|
||||
? compactEncode(classDiffs)
|
||||
: HeapSnapshotFormatter.formatDiffSummary(classDiffs),
|
||||
);
|
||||
structuredContent.heapSnapshotClassDiffs = classDiffs;
|
||||
@@ -1230,8 +1249,8 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
||||
if (detailedClassDiff) {
|
||||
response.push('### Heap Snapshot Detailed Diff');
|
||||
response.push(
|
||||
useToon && toonEncode
|
||||
? toonEncode(detailedClassDiff)
|
||||
compactEncode
|
||||
? compactEncode(detailedClassDiff)
|
||||
: HeapSnapshotFormatter.formatDiffDetails(detailedClassDiff),
|
||||
);
|
||||
structuredContent.heapSnapshotDetailedClassDiff = detailedClassDiff;
|
||||
@@ -1342,8 +1361,8 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
||||
i.toJSON(),
|
||||
);
|
||||
response.push(
|
||||
...(useToon && toonEncode
|
||||
? [toonEncode(structuredContent.networkRequests)]
|
||||
...(compactEncode
|
||||
? [compactEncode(structuredContent.networkRequests)]
|
||||
: paginationData.items.map(i => i.toString())),
|
||||
);
|
||||
}
|
||||
@@ -1367,8 +1386,8 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
||||
item.toJSON(),
|
||||
);
|
||||
response.push(...paginationData.info);
|
||||
if (useToon && toonEncode) {
|
||||
response.push(toonEncode(structuredContent.consoleMessages));
|
||||
if (compactEncode) {
|
||||
response.push(compactEncode(structuredContent.consoleMessages));
|
||||
} else {
|
||||
response.push(...paginationData.items.map(item => item.toString()));
|
||||
}
|
||||
|
||||
+10
-1
@@ -7,6 +7,7 @@
|
||||
import type {parseArguments} from './bin/chrome-devtools-mcp-cli-options.js';
|
||||
import {logger} from './logger.js';
|
||||
import type {McpContext} from './McpContext.js';
|
||||
import type {DataFormat} from './McpResponse.js';
|
||||
import {McpResponse} from './McpResponse.js';
|
||||
import type {Mutex} from './Mutex.js';
|
||||
import {SlimMcpResponse} from './SlimMcpResponse.js';
|
||||
@@ -259,10 +260,18 @@ export class ToolHandler {
|
||||
} catch (err) {
|
||||
response.setError(err);
|
||||
}
|
||||
// Resolve data format: --experimentalDataFormat takes precedence, fall back to legacy --experimentalToonFormat
|
||||
let dataFormat: DataFormat = 'default';
|
||||
if (this.serverArgs.experimentalDataFormat) {
|
||||
dataFormat = this.serverArgs.experimentalDataFormat as DataFormat;
|
||||
} else if (this.serverArgs.experimentalToonFormat) {
|
||||
dataFormat = 'toon';
|
||||
}
|
||||
|
||||
const {content, structuredContent} = await response.handle(
|
||||
this.tool.name,
|
||||
context,
|
||||
this.serverArgs.experimentalToonFormat ?? false,
|
||||
dataFormat,
|
||||
);
|
||||
const result: CallToolResult & {
|
||||
structuredContent?: Record<string, unknown>;
|
||||
|
||||
@@ -173,7 +173,14 @@ export const cliOptions = {
|
||||
experimentalToonFormat: {
|
||||
type: 'boolean',
|
||||
describe:
|
||||
'Whether to format structured data in text response using Token-Oriented Object Notation (requires @toon-format/toon). If running via npx, use: npx --package chrome-devtools-mcp@latest --package @toon-format/toon@latest chrome-devtools-mcp --experimentalToonFormat',
|
||||
'Deprecated: use --experimentalDataFormat=toon instead. Whether to format structured data using TOON (requires @toon-format/toon).',
|
||||
hidden: true,
|
||||
},
|
||||
experimentalDataFormat: {
|
||||
type: 'string',
|
||||
describe:
|
||||
'Override format for structured data in text responses. Default uses built-in formatters. "toon" (requires @toon-format/toon) or "gcf" (requires @blackwell-systems/gcf) replace structured content with the specified encoding.',
|
||||
choices: ['default', 'toon', 'gcf'] as const,
|
||||
hidden: true,
|
||||
},
|
||||
experimentalIncludeAllPages: {
|
||||
|
||||
@@ -347,5 +347,19 @@
|
||||
{
|
||||
"name": "screenshot_max_height_present",
|
||||
"flagType": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "experimental_data_format_present",
|
||||
"flagType": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "experimental_data_format",
|
||||
"flagType": "enum",
|
||||
"choices": [
|
||||
"EXPERIMENTAL_DATA_FORMAT_UNSPECIFIED",
|
||||
"EXPERIMENTAL_DATA_FORMAT_DEFAULT",
|
||||
"EXPERIMENTAL_DATA_FORMAT_TOON",
|
||||
"EXPERIMENTAL_DATA_FORMAT_GCF"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Vendored
+4
@@ -61,6 +61,10 @@ export async function getToonEncode(): Promise<(val: unknown) => string> {
|
||||
const {encode} = await import('@toon-format/toon');
|
||||
return encode;
|
||||
}
|
||||
export async function getGcfEncode(): Promise<(val: unknown) => string> {
|
||||
const {encodeGeneric} = await import('@blackwell-systems/gcf');
|
||||
return encodeGeneric;
|
||||
}
|
||||
|
||||
import {
|
||||
snapshot as snapshotImpl,
|
||||
|
||||
Reference in New Issue
Block a user