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:
Dayna Blackwell
2026-07-06 02:45:01 -07:00
committed by GitHub
parent b06e39b164
commit 3d2138945c
8 changed files with 104 additions and 24 deletions
+15
View File
@@ -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
View File
@@ -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": {
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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>;
+8 -1
View File
@@ -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: {
+14
View File
@@ -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"
]
}
]
+4
View File
@@ -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,