chromedevtools--chrome-devtools-mcp
3d2138945c
## 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>
321 行
9.4 KiB
JavaScript
321 行
9.4 KiB
JavaScript
/**
|
|
* Copyright 2021 Google LLC.
|
|
* Copyright (c) Microsoft Corporation.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview taken from {@link https://github.com/GoogleChromeLabs/chromium-bidi/blob/main/rollup.config.mjs | chromium-bidi}
|
|
* and modified to specific requirement.
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import commonjs from '@rollup/plugin-commonjs';
|
|
import json from '@rollup/plugin-json';
|
|
import {nodeResolve} from '@rollup/plugin-node-resolve';
|
|
import cleanup from 'rollup-plugin-cleanup';
|
|
import license from 'rollup-plugin-license';
|
|
|
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
|
|
const allowedLicenses = [
|
|
'MIT',
|
|
'Apache 2.0',
|
|
'Apache-2.0',
|
|
'BSD-3-Clause',
|
|
'BSD-2-Clause',
|
|
'ISC',
|
|
'0BSD',
|
|
];
|
|
|
|
const thirdPartyDir = './build/src/third_party';
|
|
|
|
const {devDependencies = {}} = JSON.parse(
|
|
fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'),
|
|
);
|
|
|
|
// special case for puppeteer, from which we only bundle puppeteer-core
|
|
devDependencies['puppeteer-core'] = devDependencies['puppeteer'];
|
|
|
|
const aggregatedStats = {
|
|
bundlesProcessed: 0,
|
|
totalBundles: 0,
|
|
bundledPackages: new Set(),
|
|
};
|
|
|
|
const projectNodeModulesPath =
|
|
path.join(process.cwd(), 'node_modules') + path.sep;
|
|
|
|
function getPackageName(modulePath) {
|
|
// Handle rollup's virtual module paths (paths starting with 0x00)
|
|
const absolutePathStart = modulePath.indexOf(projectNodeModulesPath);
|
|
if (absolutePathStart < 0) {
|
|
return null;
|
|
}
|
|
|
|
const relativePath = modulePath.slice(
|
|
projectNodeModulesPath.length + absolutePathStart,
|
|
);
|
|
const segments = relativePath.split(path.sep);
|
|
|
|
// handle scoped packages
|
|
if (segments[0].startsWith('@') && segments[1]) {
|
|
return `${segments[0]}/${segments[1]}`;
|
|
}
|
|
return segments[0];
|
|
}
|
|
|
|
/**
|
|
* @returns {import('rollup').Plugin}
|
|
*/
|
|
function listBundledDeps() {
|
|
aggregatedStats.totalBundles++;
|
|
return {
|
|
name: 'gather-bundled-dependencies',
|
|
generateBundle(options, bundle) {
|
|
for (const chunk of Object.values(bundle)) {
|
|
if (chunk.type === 'chunk' && chunk.modules) {
|
|
// chunk.modules is an object where keys are the absolute file paths
|
|
Object.keys(chunk.modules).forEach(modulePath => {
|
|
const packageName = getPackageName(modulePath);
|
|
if (packageName) {
|
|
aggregatedStats.bundledPackages.add(packageName);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
aggregatedStats.bundlesProcessed++;
|
|
|
|
// Only write the file when the last bundle is finished
|
|
if (aggregatedStats.bundlesProcessed === aggregatedStats.totalBundles) {
|
|
const outputPath = path.join(thirdPartyDir, 'bundled-packages.json');
|
|
|
|
const bundledDevDeps = Object.fromEntries(
|
|
Object.entries(devDependencies).filter(
|
|
([name]) =>
|
|
aggregatedStats.bundledPackages.has(name) ||
|
|
name === 'chrome-devtools-frontend' ||
|
|
name === 'lighthouse',
|
|
),
|
|
);
|
|
|
|
fs.writeFileSync(outputPath, JSON.stringify(bundledDevDeps, null, 2));
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
const seenDependencies = new Map();
|
|
|
|
/**
|
|
* @param {string} wrapperIndexName
|
|
* @param {import('rollup').OutputOptions} [extraOutputOptions={}]
|
|
* @param {import('rollup').ExternalOption} [external=[]]
|
|
* @returns {import('rollup').RollupOptions}
|
|
*/
|
|
const bundleDependency = (
|
|
wrapperIndexName,
|
|
extraOutputOptions = {},
|
|
external = [],
|
|
) => ({
|
|
input: path.join(thirdPartyDir, wrapperIndexName),
|
|
output: {
|
|
...extraOutputOptions,
|
|
file: path.join(thirdPartyDir, wrapperIndexName),
|
|
sourcemap: !isProduction,
|
|
format: 'esm',
|
|
},
|
|
plugins: [
|
|
cleanup({
|
|
// Keep license comments. Other comments are removed due to
|
|
// http://b/390559299 and
|
|
// https://github.com/microsoft/TypeScript/issues/60811.
|
|
comments: [/Copyright/i],
|
|
}),
|
|
license({
|
|
thirdParty: {
|
|
allow: {
|
|
test: dependency => {
|
|
return allowedLicenses.includes(dependency.license);
|
|
},
|
|
failOnUnlicensed: true,
|
|
failOnViolation: true,
|
|
},
|
|
output: {
|
|
file: path.join(thirdPartyDir, 'THIRD_PARTY_NOTICES'),
|
|
template(dependencies) {
|
|
for (const dependency of dependencies) {
|
|
const key = `${dependency.name}:${dependency.version}`;
|
|
seenDependencies.set(key, dependency);
|
|
}
|
|
|
|
const stringifiedDependencies = Array.from(
|
|
seenDependencies.values(),
|
|
).map(dependency => {
|
|
let arr = [];
|
|
arr.push(`Name: ${dependency.name ?? 'N/A'}`);
|
|
let url = dependency.homepage ?? dependency.repository;
|
|
if (url !== null && typeof url !== 'string') {
|
|
url = url.url;
|
|
}
|
|
arr.push(`URL: ${url ?? 'N/A'}`);
|
|
arr.push(`Version: ${dependency.version ?? 'N/A'}`);
|
|
arr.push(`License: ${dependency.license ?? 'N/A'}`);
|
|
if (dependency.licenseText !== null) {
|
|
arr.push('');
|
|
arr.push(dependency.licenseText.replaceAll('\r', ''));
|
|
}
|
|
return arr.join('\n');
|
|
});
|
|
|
|
// Manual license handling for chrome-devtools-frontend third_party
|
|
const tsConfig = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(process.cwd(), 'tsconfig.json'),
|
|
'utf-8',
|
|
),
|
|
);
|
|
const thirdPartyDirectories = tsConfig.include.filter(location =>
|
|
location.includes(
|
|
'node_modules/chrome-devtools-frontend/front_end/third_party',
|
|
),
|
|
);
|
|
|
|
const manualLicenses = [];
|
|
// Add chrome-devtools-frontend main license
|
|
const cdtfLicensePath = path.join(
|
|
process.cwd(),
|
|
'node_modules/chrome-devtools-frontend/LICENSE',
|
|
);
|
|
if (fs.existsSync(cdtfLicensePath)) {
|
|
manualLicenses.push(
|
|
[
|
|
'Name: chrome-devtools-frontend',
|
|
'License: Apache-2.0',
|
|
'',
|
|
fs.readFileSync(cdtfLicensePath, 'utf-8'),
|
|
].join('\n'),
|
|
);
|
|
}
|
|
|
|
// Add chrome-devtools-frontend main license
|
|
const lighthouseLicensePath = path.join(
|
|
process.cwd(),
|
|
'node_modules/lighthouse/LICENSE',
|
|
);
|
|
if (fs.existsSync(lighthouseLicensePath)) {
|
|
manualLicenses.push(
|
|
[
|
|
'Name: lighthouse',
|
|
'License: Apache-2.0',
|
|
'',
|
|
fs.readFileSync(lighthouseLicensePath, 'utf-8'),
|
|
].join('\n'),
|
|
);
|
|
}
|
|
|
|
for (const thirdPartyDir of thirdPartyDirectories) {
|
|
const fullPath = path.join(process.cwd(), thirdPartyDir);
|
|
const licenseFile = path.join(fullPath, 'LICENSE');
|
|
if (fs.existsSync(licenseFile)) {
|
|
const name = path.basename(thirdPartyDir);
|
|
manualLicenses.push(
|
|
[
|
|
`Name: ${name}`,
|
|
`License:`,
|
|
'',
|
|
fs.readFileSync(licenseFile, 'utf-8').replaceAll('\r', ''),
|
|
].join('\n'),
|
|
);
|
|
}
|
|
}
|
|
|
|
if (manualLicenses.length > 0) {
|
|
stringifiedDependencies.push(...manualLicenses);
|
|
}
|
|
|
|
const divider =
|
|
'\n\n-------------------- DEPENDENCY DIVIDER --------------------\n\n';
|
|
return stringifiedDependencies.join(divider);
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
listBundledDeps(),
|
|
commonjs(),
|
|
json(),
|
|
nodeResolve(),
|
|
],
|
|
external,
|
|
});
|
|
|
|
export default [
|
|
bundleDependency(
|
|
'index.js',
|
|
{
|
|
inlineDynamicImports: true,
|
|
},
|
|
(source, importer, _isResolved) => {
|
|
if (
|
|
source === 'yargs' &&
|
|
importer &&
|
|
importer.includes('puppeteer-core')
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
source === '@toon-format/toon' ||
|
|
source.startsWith('@toon-format/toon/')
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
source === '@blackwell-systems/gcf' ||
|
|
source.startsWith('@blackwell-systems/gcf/')
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const existingExternals = [
|
|
'./bidi.js',
|
|
'../bidi/bidi.js',
|
|
'./lighthouse-devtools-mcp-bundle.js',
|
|
];
|
|
|
|
if (existingExternals.includes(source)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
),
|
|
bundleDependency(
|
|
'devtools-formatter-worker.js',
|
|
{
|
|
inlineDynamicImports: true,
|
|
},
|
|
(_source, _importer, _isResolved) => false,
|
|
),
|
|
bundleDependency(
|
|
'devtools-heap-snapshot-worker.js',
|
|
{
|
|
inlineDynamicImports: true,
|
|
},
|
|
(_source, _importer, _isResolved) => false,
|
|
),
|
|
];
|