From ea498ffa226e4a174517d0caf6914dbf3ffd9535 Mon Sep 17 00:00:00 2001 From: Piotr Paulski Date: Tue, 12 May 2026 15:14:49 +0000 Subject: [PATCH] Add option to encode structuredContent as TOON --- package-lock.json | 8 +++ package.json | 1 + src/McpResponse.ts | 72 ++++++++++++++-------- src/ToolHandler.ts | 1 + src/bin/chrome-devtools-mcp-cli-options.ts | 9 ++- src/telemetry/flag_usage_metrics.json | 8 +++ src/third_party/index.ts | 1 + tests/third_party_notices.test.js.snapshot | 30 +++++++++ 8 files changed, 103 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 55723ae9c..09fdbf3ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "@stylistic/eslint-plugin": "^5.4.0", + "@toon-format/toon": "^2.2.0", "@types/debug": "^4.1.12", "@types/filesystem": "^0.0.36", "@types/node": "^25.0.0", @@ -2036,6 +2037,13 @@ "eslint": "^9.0.0 || ^10.0.0" } }, + "node_modules/@toon-format/toon": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-2.2.0.tgz", + "integrity": "sha512-FMYqrlZnMN72YIT9KVt7Kxc41gat+RgMIzDmvRRPHw0J7pqW/FeBGDY/4BIWjT71Y+EdI9fCJip90uXuGuYhjw==", + "dev": true, + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", diff --git a/package.json b/package.json index 307f26877..cf00234f4 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "@stylistic/eslint-plugin": "^5.4.0", + "@toon-format/toon": "^2.2.0", "@types/debug": "^4.1.12", "@types/filesystem": "^0.0.36", "@types/node": "^25.0.0", diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 7da6b58dc..217066cca 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -17,7 +17,7 @@ import type {McpContext} from './McpContext.js'; import type {McpPage} from './McpPage.js'; import {UncaughtError} from './PageCollector.js'; import {TextSnapshot} from './TextSnapshot.js'; -import {DevTools, type Protocol} from './third_party/index.js'; +import {DevTools, toonEncode, type Protocol} from './third_party/index.js'; import type { ConsoleMessage, ImageContent, @@ -498,6 +498,7 @@ export class McpResponse implements Response { async handle( toolName: string, context: McpContext, + useToon = false, ): Promise<{ content: Array; structuredContent: object; @@ -726,20 +727,25 @@ export class McpResponse implements Response { } } - return this.format(toolName, context, { - detailedConsoleMessage, - consoleMessages, - snapshot, - detailedNetworkRequest, - networkRequests, - traceInsight: this.#attachedTraceInsight, - traceSummary: this.#attachedTraceSummary, - extensions, - lighthouseResult: this.#attachedLighthouseResult, - thirdPartyDeveloperTools, - webmcpTools, - errorMessage: this.#error?.message, - }); + return this.format( + toolName, + context, + { + detailedConsoleMessage, + consoleMessages, + snapshot, + detailedNetworkRequest, + networkRequests, + traceInsight: this.#attachedTraceInsight, + traceSummary: this.#attachedTraceSummary, + extensions, + lighthouseResult: this.#attachedLighthouseResult, + thirdPartyDeveloperTools, + webmcpTools, + errorMessage: this.#error?.message, + }, + useToon, + ); } format( @@ -759,6 +765,7 @@ export class McpResponse implements Response { webmcpTools?: WebMCPTool[]; errorMessage?: string; }, + useToon: boolean, ): {content: Array; structuredContent: object} { const structuredContent: { snapshot?: object; @@ -1006,9 +1013,13 @@ Call ${handleDialog.name} to handle it before continuing.`); response.push(`Saved snapshot to ${data.snapshot}.`); structuredContent.snapshotFilePath = data.snapshot; } else { - response.push('## Latest page snapshot'); - response.push(data.snapshot.toString()); structuredContent.snapshot = data.snapshot.toJSON(); + response.push('## Latest page snapshot'); + response.push( + useToon + ? toonEncode(structuredContent.snapshot) + : data.snapshot.toString(), + ); } } @@ -1041,8 +1052,12 @@ Call ${handleDialog.name} to handle it before continuing.`); const paginatedRecord = Object.fromEntries(paginationData.items); const formatter = new HeapSnapshotFormatter(paginatedRecord); - response.push(formatter.toString()); structuredContent.heapSnapshotData = formatter.toJSON(); + response.push( + useToon + ? toonEncode(structuredContent.heapSnapshotData) + : formatter.toString(), + ); } const nodes = this.#heapSnapshotOptions.nodes; if (nodes) { @@ -1154,11 +1169,14 @@ Call ${handleDialog.name} to handle it before continuing.`); structuredContent.pagination = paginationData.pagination; response.push(...paginationData.info); if (data.networkRequests) { - structuredContent.networkRequests = []; - for (const formatter of paginationData.items) { - response.push(formatter.toString()); - structuredContent.networkRequests.push(formatter.toJSON()); - } + structuredContent.networkRequests = paginationData.items.map(i => + i.toJSON(), + ); + response.push( + ...(useToon + ? [toonEncode(structuredContent.networkRequests)] + : paginationData.items.map(i => i.toString())), + ); } } else { response.push('No requests found.'); @@ -1176,11 +1194,15 @@ Call ${handleDialog.name} to handle it before continuing.`); this.#consoleDataOptions.pagination, ); structuredContent.pagination = paginationData.pagination; - response.push(...paginationData.info); - response.push(...paginationData.items.map(item => item.toString())); structuredContent.consoleMessages = paginationData.items.map(item => item.toJSON(), ); + response.push(...paginationData.info); + if (useToon) { + response.push(toonEncode(structuredContent.consoleMessages)); + } else { + response.push(...paginationData.items.map(item => item.toString())); + } } else { response.push(''); } diff --git a/src/ToolHandler.ts b/src/ToolHandler.ts index 042d6701f..68de03081 100644 --- a/src/ToolHandler.ts +++ b/src/ToolHandler.ts @@ -262,6 +262,7 @@ export class ToolHandler { const {content, structuredContent} = await response.handle( this.tool.name, context, + this.serverArgs.experimentalToonFormat ?? false, ); const result: CallToolResult & { structuredContent?: Record; diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index 0cfb3c2b8..43d5c2360 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -170,6 +170,12 @@ export const cliOptions = { type: 'boolean', describe: 'Whether to output structured formatted content.', }, + experimentalToonFormat: { + type: 'boolean', + describe: + 'Whether to format structured data in text response using Token-Oriented Object Notation. Defaults to false which represents the embedded content as formatted JSON instead.', + hidden: true, + }, experimentalIncludeAllPages: { type: 'boolean', describe: @@ -306,7 +312,7 @@ export function parseArguments( const yargsInstance = yargs(hideBin(argv)) .scriptName('npx chrome-devtools-mcp@latest') .options(cliOptions) - .check(args => { + .middleware(args => { // We can't set default in the options else // Yargs will complain if ( @@ -323,7 +329,6 @@ export function parseArguments( ); args.usageStatistics = false; } - return true; }) .example([ [ diff --git a/src/telemetry/flag_usage_metrics.json b/src/telemetry/flag_usage_metrics.json index a273b5ebe..9435cca3c 100644 --- a/src/telemetry/flag_usage_metrics.json +++ b/src/telemetry/flag_usage_metrics.json @@ -313,5 +313,13 @@ { "name": "allowed_url_pattern_present", "flagType": "boolean" + }, + { + "name": "experimental_toon_format_present", + "flagType": "boolean" + }, + { + "name": "experimental_toon_format", + "flagType": "boolean" } ] diff --git a/src/third_party/index.ts b/src/third_party/index.ts index b7ffb6576..da1907765 100644 --- a/src/third_party/index.ts +++ b/src/third_party/index.ts @@ -57,6 +57,7 @@ export { Browser as BrowserEnum, type ChromeReleaseChannel as BrowsersChromeReleaseChannel, } from '@puppeteer/browsers'; +export {encode as toonEncode} from '@toon-format/toon'; import { snapshot as snapshotImpl, diff --git a/tests/third_party_notices.test.js.snapshot b/tests/third_party_notices.test.js.snapshot index 4b9412642..4cce074ad 100644 --- a/tests/third_party_notices.test.js.snapshot +++ b/tests/third_party_notices.test.js.snapshot @@ -907,6 +907,36 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------- DEPENDENCY DIVIDER -------------------- + +Name: @toon-format/toon +URL: https://toonformat.dev +Version: +License: MIT + +MIT License + +Copyright (c) 2025-PRESENT Johann Schopplich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + -------------------- DEPENDENCY DIVIDER -------------------- Name: chrome-devtools-frontend