From 9fa46ee04d023f8c5db758307a9dd52f72aa589f Mon Sep 17 00:00:00 2001 From: Marc Andreu Date: Thu, 20 Nov 2025 17:23:01 +0900 Subject: [PATCH 1/4] fix: detect actual image format in take_screenshot tool Fixes #583 The take_screenshot tool was incorrectly declaring the MIME type based on the requested format parameter rather than detecting the actual format of the image data returned by Puppeteer. This caused a mismatch when Puppeteer returned a different format than requested, leading to Claude API validation errors like: "Image does not match the provided media type image/jpeg" Changes: - Add detectImageFormat() utility to identify actual image format from binary data by inspecting magic numbers (PNG, JPEG, WebP) - Update screenshot tool to detect and use the actual format instead of blindly trusting the request parameter - Add comprehensive unit tests for format detection Co-Authored-By: Claude --- src/tools/screenshot.ts | 9 +++- src/utils/imageFormat.ts | 55 +++++++++++++++++++++++ tests/utils/imageFormat.test.ts | 77 +++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 src/utils/imageFormat.ts create mode 100644 tests/utils/imageFormat.test.ts diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts index d8d3dfcf..9d80bd9f 100644 --- a/src/tools/screenshot.ts +++ b/src/tools/screenshot.ts @@ -7,6 +7,7 @@ import {zod} from '../third_party/index.js'; import type {ElementHandle, Page} from '../third_party/index.js'; +import {detectImageFormat} from '../utils/imageFormat.js'; import {ToolCategory} from './categories.js'; import {defineTool} from './ToolDefinition.js'; @@ -83,18 +84,22 @@ export const screenshot = defineTool({ ); } + // Detect the actual format of the screenshot data + // Puppeteer may not always return the requested format + const actualFormat = detectImageFormat(screenshot); + if (request.params.filePath) { const file = await context.saveFile(screenshot, request.params.filePath); response.appendResponseLine(`Saved screenshot to ${file.filename}.`); } else if (screenshot.length >= 2_000_000) { const {filename} = await context.saveTemporaryFile( screenshot, - `image/${request.params.format}`, + actualFormat, ); response.appendResponseLine(`Saved screenshot to ${filename}.`); } else { response.attachImage({ - mimeType: `image/${request.params.format}`, + mimeType: actualFormat, data: Buffer.from(screenshot).toString('base64'), }); } diff --git a/src/utils/imageFormat.ts b/src/utils/imageFormat.ts new file mode 100644 index 00000000..2ec22a77 --- /dev/null +++ b/src/utils/imageFormat.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Detects the actual image format from binary data by inspecting magic numbers. + * + * @param data - The image data as a Uint8Array or Buffer + * @returns The detected MIME type ('image/png', 'image/jpeg', or 'image/webp') + * @throws Error if the format cannot be detected + */ +export function detectImageFormat( + data: Uint8Array | Buffer, +): 'image/png' | 'image/jpeg' | 'image/webp' { + if (data.length < 12) { + throw new Error('Image data too small to detect format'); + } + + // Check PNG: starts with 89 50 4E 47 (‰PNG) + if ( + data[0] === 0x89 && + data[1] === 0x50 && + data[2] === 0x4e && + data[3] === 0x47 + ) { + return 'image/png'; + } + + // Check JPEG: starts with FF D8 FF + if (data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) { + return 'image/jpeg'; + } + + // Check WebP: starts with "RIFF" and contains "WEBP" at offset 8 + if ( + data[0] === 0x52 && // R + data[1] === 0x49 && // I + data[2] === 0x46 && // F + data[3] === 0x46 && // F + data[8] === 0x57 && // W + data[9] === 0x45 && // E + data[10] === 0x42 && // B + data[11] === 0x50 // P + ) { + return 'image/webp'; + } + + throw new Error( + `Unable to detect image format. First bytes: ${Array.from(data.slice(0, 12)) + .map(b => b.toString(16).padStart(2, '0')) + .join(' ')}`, + ); +} diff --git a/tests/utils/imageFormat.test.ts b/tests/utils/imageFormat.test.ts new file mode 100644 index 00000000..d0d9623b --- /dev/null +++ b/tests/utils/imageFormat.test.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'node:assert'; +import {describe, it} from 'node:test'; + +import {detectImageFormat} from '../../src/utils/imageFormat.js'; + +describe('imageFormat', () => { + describe('detectImageFormat', () => { + it('detects PNG format', () => { + // PNG magic number: 89 50 4E 47 0D 0A 1A 0A + const pngData = new Uint8Array([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + ]); + const format = detectImageFormat(pngData); + assert.equal(format, 'image/png'); + }); + + it('detects JPEG format', () => { + // JPEG magic number: FF D8 FF + const jpegData = new Uint8Array([ + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, + ]); + const format = detectImageFormat(jpegData); + assert.equal(format, 'image/jpeg'); + }); + + it('detects WebP format', () => { + // WebP magic number: RIFF ... WEBP + const webpData = new Uint8Array([ + 0x52, + 0x49, + 0x46, + 0x46, // RIFF + 0x00, + 0x00, + 0x00, + 0x00, // file size (placeholder) + 0x57, + 0x45, + 0x42, + 0x50, // WEBP + ]); + const format = detectImageFormat(webpData); + assert.equal(format, 'image/webp'); + }); + + it('throws error for data that is too small', () => { + const smallData = new Uint8Array([0x89, 0x50]); + assert.throws( + () => detectImageFormat(smallData), + /Image data too small to detect format/, + ); + }); + + it('throws error for unknown format', () => { + const unknownData = new Uint8Array([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]); + assert.throws( + () => detectImageFormat(unknownData), + /Unable to detect image format/, + ); + }); + + it('works with Buffer objects', () => { + const pngBuffer = Buffer.from([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + ]); + const format = detectImageFormat(pngBuffer); + assert.equal(format, 'image/png'); + }); + }); +}); From 6d090cfd923eb08ddfa819a2821b25c1b2d1dad8 Mon Sep 17 00:00:00 2001 From: Marc Andreu Date: Thu, 20 Nov 2025 23:00:07 +0900 Subject: [PATCH 2/4] feat: add default screenshot format configuration and update related tools --- README.md | 29 +++++++++++++++++++++++++++++ src/McpContext.ts | 6 ++++++ src/cli.ts | 11 +++++++++++ src/main.ts | 1 + src/tools/ToolDefinition.ts | 4 ++++ src/tools/screenshot.ts | 10 +++++++--- 6 files changed, 58 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 07f598be..fffeb649 100644 --- a/README.md +++ b/README.md @@ -383,6 +383,12 @@ The Chrome DevTools MCP server supports the following configuration option: - **Type:** boolean - **Default:** `true` +- **`--screenshot-format`** + Default image format for screenshots. Options: png, jpeg, webp. Default is jpeg. + - **Type:** string + - **Choices:** `png`, `jpeg`, `webp` + - **Default:** `jpeg` + Pass them via the `args` property in the JSON configuration. For example: @@ -424,6 +430,29 @@ You can connect directly to a Chrome WebSocket endpoint and include custom heade To get the WebSocket endpoint from a running Chrome instance, visit `http://127.0.0.1:9222/json/version` and look for the `webSocketDebuggerUrl` field. +### Configuring default screenshot format + +You can set a default image format for all screenshots using the `--screenshot-format` option. The default is JPEG. You can change it to PNG or WebP if needed: + +```json +{ + "mcpServers": { + "chrome-devtools": { + "command": "npx", + "args": [ + "chrome-devtools-mcp@latest", + "--screenshot-format=png" + ] + } + } +} +``` + +When configured, the `take_screenshot` tool will use this format by default unless explicitly overridden by passing a `format` parameter. Supported formats are `png`, `jpeg`, and `webp`. + +> [!TIP] +> JPEG is the default format as it typically produces smaller file sizes than PNG, which improves performance when working with screenshots. WebP offers the best compression while maintaining quality. Use PNG if you need lossless screenshots. + You can also run `npx chrome-devtools-mcp@latest --help` to see all available configuration options. ## Concepts diff --git a/src/McpContext.ts b/src/McpContext.ts index 1c3c988d..53638659 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -49,6 +49,8 @@ interface McpContextOptions { experimentalDevToolsDebugging: boolean; // Whether all page-like targets are exposed as pages. experimentalIncludeAllPages?: boolean; + // Default screenshot format. + screenshotFormat?: 'png' | 'jpeg' | 'webp'; } const DEFAULT_TIMEOUT = 5_000; @@ -292,6 +294,10 @@ export class McpContext implements Context { this.#dialog = undefined; } + getDefaultScreenshotFormat(): 'png' | 'jpeg' | 'webp' { + return this.#options.screenshotFormat ?? 'jpeg'; + } + getSelectedPage(): Page { const page = this.#selectedPage; if (!page) { diff --git a/src/cli.ts b/src/cli.ts index 5ce8673e..750d0e6e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -160,6 +160,13 @@ export const cliOptions = { default: true, describe: 'Set to false to exclude tools related to network.', }, + screenshotFormat: { + type: 'string', + describe: + 'Default image format for screenshots. Options: png, jpeg, webp. Default is png.', + choices: ['png', 'jpeg', 'webp'] as const, + default: 'png', + }, } satisfies Record; export function parseArguments(version: string, argv = process.argv) { @@ -212,6 +219,10 @@ export function parseArguments(version: string, argv = process.argv) { 'Disable tools in the performance category', ], ['$0 --no-category-network', 'Disable tools in the network category'], + [ + '$0 --screenshot-format jpeg', + 'Use JPEG as the default screenshot format', + ], ]); return yargsInstance diff --git a/src/main.ts b/src/main.ts index 153b6f5d..77662726 100644 --- a/src/main.ts +++ b/src/main.ts @@ -86,6 +86,7 @@ async function getContext(): Promise { context = await McpContext.from(browser, logger, { experimentalDevToolsDebugging: devtools, experimentalIncludeAllPages: args.experimentalIncludeAllPages, + screenshotFormat: args.screenshotFormat as 'png' | 'jpeg' | 'webp', }); } return context; diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 8acebb56..ad44a3ad 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -120,6 +120,10 @@ export type Context = Readonly<{ * Returns a reqid for a cdpRequestId. */ resolveCdpElementId(cdpBackendNodeId: number): string | undefined; + /** + * Returns the configured default screenshot format. + */ + getDefaultScreenshotFormat(): 'png' | 'jpeg' | 'webp'; }>; export function defineTool( diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts index 9d80bd9f..c2d9eee1 100644 --- a/src/tools/screenshot.ts +++ b/src/tools/screenshot.ts @@ -22,8 +22,8 @@ export const screenshot = defineTool({ schema: { format: zod .enum(['png', 'jpeg', 'webp']) - .default('png') - .describe('Type of format to save the screenshot as. Default is "png"'), + .optional() + .describe('Type of format to save the screenshot as. If not specified, uses the configured default format.'), quality: zod .number() .min(0) @@ -63,8 +63,11 @@ export const screenshot = defineTool({ pageOrHandle = context.getSelectedPage(); } + // Use configured default format if not specified in request + const format = request.params.format ?? context.getDefaultScreenshotFormat(); + const screenshot = await pageOrHandle.screenshot({ - type: request.params.format, + type: format, fullPage: request.params.fullPage, quality: request.params.quality, optimizeForSpeed: true, // Bonus: optimize encoding for speed @@ -87,6 +90,7 @@ export const screenshot = defineTool({ // Detect the actual format of the screenshot data // Puppeteer may not always return the requested format const actualFormat = detectImageFormat(screenshot); + console.error(`[DEBUG] Requested format: ${format}, Detected format: ${actualFormat}`); if (request.params.filePath) { const file = await context.saveFile(screenshot, request.params.filePath); From c941c89f7027135f872d7383343d0b2f878abd1c Mon Sep 17 00:00:00 2001 From: Marc Andreu Date: Thu, 20 Nov 2025 23:17:20 +0900 Subject: [PATCH 3/4] feat: update default screenshot format to jpeg in CLI options --- src/cli.ts | 4 ++-- tests/cli.test.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 750d0e6e..1bdd6e00 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -163,9 +163,9 @@ export const cliOptions = { screenshotFormat: { type: 'string', describe: - 'Default image format for screenshots. Options: png, jpeg, webp. Default is png.', + 'Default image format for screenshots. Options: png, jpeg, webp. Default is jpeg.', choices: ['png', 'jpeg', 'webp'] as const, - default: 'png', + default: 'jpeg', }, } satisfies Record; diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 19502e28..96d168c9 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -16,6 +16,8 @@ describe('cli args parsing', () => { categoryPerformance: true, 'category-network': true, categoryNetwork: true, + 'screenshot-format': 'jpeg', + screenshotFormat: 'jpeg', }; it('parses with default args', async () => { From aeab5274963dc7c600fc09919e06ec372e8fe9da Mon Sep 17 00:00:00 2001 From: Marc Andreu Date: Fri, 21 Nov 2025 10:20:13 +0900 Subject: [PATCH 4/4] feat: change default screenshot format to png and update related documentation --- README.md | 13 +++--- src/cli.ts | 4 +- src/tools/screenshot.ts | 10 +---- src/utils/imageFormat.ts | 55 ----------------------- tests/cli.test.ts | 4 +- tests/utils/imageFormat.test.ts | 77 --------------------------------- 6 files changed, 14 insertions(+), 149 deletions(-) delete mode 100644 src/utils/imageFormat.ts delete mode 100644 tests/utils/imageFormat.test.ts diff --git a/README.md b/README.md index fffeb649..d04050e1 100644 --- a/README.md +++ b/README.md @@ -384,10 +384,10 @@ The Chrome DevTools MCP server supports the following configuration option: - **Default:** `true` - **`--screenshot-format`** - Default image format for screenshots. Options: png, jpeg, webp. Default is jpeg. + Default image format for screenshots. Options: png, jpeg, webp. Default is png. - **Type:** string - **Choices:** `png`, `jpeg`, `webp` - - **Default:** `jpeg` + - **Default:** `png` @@ -432,7 +432,7 @@ To get the WebSocket endpoint from a running Chrome instance, visit `http://127. ### Configuring default screenshot format -You can set a default image format for all screenshots using the `--screenshot-format` option. The default is JPEG. You can change it to PNG or WebP if needed: +You can set a default image format for all screenshots using the `--screenshot-format` option. The default is PNG. You can change it to JPEG or WebP if needed: ```json { @@ -441,7 +441,7 @@ You can set a default image format for all screenshots using the `--screenshot-f "command": "npx", "args": [ "chrome-devtools-mcp@latest", - "--screenshot-format=png" + "--screenshot-format=jpeg" ] } } @@ -451,7 +451,10 @@ You can set a default image format for all screenshots using the `--screenshot-f When configured, the `take_screenshot` tool will use this format by default unless explicitly overridden by passing a `format` parameter. Supported formats are `png`, `jpeg`, and `webp`. > [!TIP] -> JPEG is the default format as it typically produces smaller file sizes than PNG, which improves performance when working with screenshots. WebP offers the best compression while maintaining quality. Use PNG if you need lossless screenshots. +> PNG is the default format as it provides lossless screenshots. JPEG typically produces smaller file sizes than PNG, which improves performance when working with screenshots. WebP offers the best compression while maintaining quality. + +> [!NOTE] +> **Claude Code users**: If you experience issues with screenshots not displaying correctly, explicitly pass `jpeg` as the format parameter in all `take_screenshot()` calls until the issue is resolved on Claude Code's side. For example: `take_screenshot(format="jpeg")`. You can also run `npx chrome-devtools-mcp@latest --help` to see all available configuration options. diff --git a/src/cli.ts b/src/cli.ts index 1bdd6e00..750d0e6e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -163,9 +163,9 @@ export const cliOptions = { screenshotFormat: { type: 'string', describe: - 'Default image format for screenshots. Options: png, jpeg, webp. Default is jpeg.', + 'Default image format for screenshots. Options: png, jpeg, webp. Default is png.', choices: ['png', 'jpeg', 'webp'] as const, - default: 'jpeg', + default: 'png', }, } satisfies Record; diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts index c2d9eee1..3eb0ead7 100644 --- a/src/tools/screenshot.ts +++ b/src/tools/screenshot.ts @@ -7,7 +7,6 @@ import {zod} from '../third_party/index.js'; import type {ElementHandle, Page} from '../third_party/index.js'; -import {detectImageFormat} from '../utils/imageFormat.js'; import {ToolCategory} from './categories.js'; import {defineTool} from './ToolDefinition.js'; @@ -87,23 +86,18 @@ export const screenshot = defineTool({ ); } - // Detect the actual format of the screenshot data - // Puppeteer may not always return the requested format - const actualFormat = detectImageFormat(screenshot); - console.error(`[DEBUG] Requested format: ${format}, Detected format: ${actualFormat}`); - if (request.params.filePath) { const file = await context.saveFile(screenshot, request.params.filePath); response.appendResponseLine(`Saved screenshot to ${file.filename}.`); } else if (screenshot.length >= 2_000_000) { const {filename} = await context.saveTemporaryFile( screenshot, - actualFormat, + `image/${format}`, ); response.appendResponseLine(`Saved screenshot to ${filename}.`); } else { response.attachImage({ - mimeType: actualFormat, + mimeType: `image/${format}`, data: Buffer.from(screenshot).toString('base64'), }); } diff --git a/src/utils/imageFormat.ts b/src/utils/imageFormat.ts deleted file mode 100644 index 2ec22a77..00000000 --- a/src/utils/imageFormat.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Detects the actual image format from binary data by inspecting magic numbers. - * - * @param data - The image data as a Uint8Array or Buffer - * @returns The detected MIME type ('image/png', 'image/jpeg', or 'image/webp') - * @throws Error if the format cannot be detected - */ -export function detectImageFormat( - data: Uint8Array | Buffer, -): 'image/png' | 'image/jpeg' | 'image/webp' { - if (data.length < 12) { - throw new Error('Image data too small to detect format'); - } - - // Check PNG: starts with 89 50 4E 47 (‰PNG) - if ( - data[0] === 0x89 && - data[1] === 0x50 && - data[2] === 0x4e && - data[3] === 0x47 - ) { - return 'image/png'; - } - - // Check JPEG: starts with FF D8 FF - if (data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) { - return 'image/jpeg'; - } - - // Check WebP: starts with "RIFF" and contains "WEBP" at offset 8 - if ( - data[0] === 0x52 && // R - data[1] === 0x49 && // I - data[2] === 0x46 && // F - data[3] === 0x46 && // F - data[8] === 0x57 && // W - data[9] === 0x45 && // E - data[10] === 0x42 && // B - data[11] === 0x50 // P - ) { - return 'image/webp'; - } - - throw new Error( - `Unable to detect image format. First bytes: ${Array.from(data.slice(0, 12)) - .map(b => b.toString(16).padStart(2, '0')) - .join(' ')}`, - ); -} diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 96d168c9..01837eff 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -16,8 +16,8 @@ describe('cli args parsing', () => { categoryPerformance: true, 'category-network': true, categoryNetwork: true, - 'screenshot-format': 'jpeg', - screenshotFormat: 'jpeg', + 'screenshot-format': 'png', + screenshotFormat: 'png', }; it('parses with default args', async () => { diff --git a/tests/utils/imageFormat.test.ts b/tests/utils/imageFormat.test.ts deleted file mode 100644 index d0d9623b..00000000 --- a/tests/utils/imageFormat.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -import assert from 'node:assert'; -import {describe, it} from 'node:test'; - -import {detectImageFormat} from '../../src/utils/imageFormat.js'; - -describe('imageFormat', () => { - describe('detectImageFormat', () => { - it('detects PNG format', () => { - // PNG magic number: 89 50 4E 47 0D 0A 1A 0A - const pngData = new Uint8Array([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, - ]); - const format = detectImageFormat(pngData); - assert.equal(format, 'image/png'); - }); - - it('detects JPEG format', () => { - // JPEG magic number: FF D8 FF - const jpegData = new Uint8Array([ - 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, - ]); - const format = detectImageFormat(jpegData); - assert.equal(format, 'image/jpeg'); - }); - - it('detects WebP format', () => { - // WebP magic number: RIFF ... WEBP - const webpData = new Uint8Array([ - 0x52, - 0x49, - 0x46, - 0x46, // RIFF - 0x00, - 0x00, - 0x00, - 0x00, // file size (placeholder) - 0x57, - 0x45, - 0x42, - 0x50, // WEBP - ]); - const format = detectImageFormat(webpData); - assert.equal(format, 'image/webp'); - }); - - it('throws error for data that is too small', () => { - const smallData = new Uint8Array([0x89, 0x50]); - assert.throws( - () => detectImageFormat(smallData), - /Image data too small to detect format/, - ); - }); - - it('throws error for unknown format', () => { - const unknownData = new Uint8Array([ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ]); - assert.throws( - () => detectImageFormat(unknownData), - /Unable to detect image format/, - ); - }); - - it('works with Buffer objects', () => { - const pngBuffer = Buffer.from([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, - ]); - const format = detectImageFormat(pngBuffer); - assert.equal(format, 'image/png'); - }); - }); -});