Description of the bug
emulate accepts a viewport whose width or height is fractional (800.5x600x1) or above Chrome's limit (20000000x600x1). Chrome rejects the resulting Emulation.setDeviceMetricsOverride call, and the tool returns a raw protocol error, but the rejected viewport has already been stored as the page's emulation state. From then on every response on that page prints Emulating viewport: {...,"width":800.5,...} although the page is not emulated, and every lighthouse_audit on the page runs the audit and then fails with the same protocol error, discarding the result. The state stays poisoned until the agent calls emulate again without the bad viewport.
Actual behavior
Both builds:
emulate {viewport:"800.5x600x1"} -> isError true
Emulating viewport: {"deviceScaleFactor":1,"isMobile":false,"hasTouch":false,"isLandscape":false,"width":800.5,"height":600}
Error: Protocol error (Emulation.setDeviceMetricsOverride): Invalid parameters Failed to deserialize params.width - BINDINGS: int32 value expected at position 22
evaluate_script -> {"w":1200,"h":2029,"dpr":1} (page not emulated)
take_screenshot -> isError false, response includes Emulating viewport: {...,"width":800.5,"height":600}
lighthouse_audit {mode:"snapshot"} -> isError true after about 1.3 s, same Emulation.setDeviceMetricsOverride int32 error, no audit result
emulate {viewport:"20000000x600x1"} -> isError true
Error: Protocol error (Emulation.setDeviceMetricsOverride): Width and height values must be positive, not greater than 10000000
lighthouse_audit {mode:"snapshot"} -> isError true, same "not greater than 10000000" error
emulate {} -> "Emulation configured successfully"
lighthouse_audit {mode:"snapshot"} -> normal result
Reproduction
- Serve any HTML page from a loopback HTTP server at
http://127.0.0.1:PORT/min.html, for example:
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>min</title></head><body><h1>Hello</h1></body></html>
- Start the server under test with the flags above and send these
tools/call requests in order:
{"name":"new_page","arguments":{"url":"http://127.0.0.1:PORT/min.html"}}
{"name":"emulate","arguments":{"pageId":2,"viewport":"800.5x600x1"}}
{"name":"evaluate_script","arguments":{"pageId":2,"function":"() => ({w: innerWidth, h: innerHeight, dpr: devicePixelRatio})"}}
{"name":"take_screenshot","arguments":{"pageId":2}}
{"name":"lighthouse_audit","arguments":{"pageId":2,"mode":"snapshot"}}
{"name":"emulate","arguments":{"pageId":2,"viewport":"20000000x600x1"}}
{"name":"lighthouse_audit","arguments":{"pageId":2,"mode":"snapshot"}}
{"name":"emulate","arguments":{"pageId":2}}
{"name":"lighthouse_audit","arguments":{"pageId":2,"mode":"snapshot"}}
The sequence was run once per build with identical results; an earlier independent run on both builds gave the same results, including lighthouse_audit in the default navigation mode.
Expectation
The viewport parameter is documented as '<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]' (docs/tool-reference.md, emulate), and the server already validates it before use: viewportTransform (src/tools/ToolDefinition.ts:500-545 at the main build) rejects non-positive or non-finite values with a readable Invalid viewport width ... message. A value Chrome cannot apply should either be rejected the same way, or, if it reaches Chrome and fails, leave the page's emulation state as it was, so that later responses report the emulation actually in effect and lighthouse_audit can restore it.
MCP configuration
--headless --isolated --no-usage-statistics --no-performance-crux unless the reproduction states otherwise (update checks disabled with CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS=1).
Chrome DevTools MCP version
1.10.1 (npm) and main 5ddb0a3 (local build)
Chrome version
154.0.8037.98 (stable)
Node version
v22.23.2
Operating system
macOS 27.0
Environment details
- chrome-devtools-mcp 1.10.1 from npm (the release build).
- chrome-devtools-mcp built from
main at commit 5ddb0a3 (the main build).
- Google Chrome 154.0.8037.98 stable, headless; Node v22.23.2; macOS 27.
- Both builds were started with
--headless --isolated --no-usage-statistics --no-performance-crux and CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS=1, and driven by a scripted stdio MCP client.
Evidence
- Expected source:
emulate.viewport format in docs/tool-reference.md and the existing input validation in viewportTransform (src/tools/ToolDefinition.ts:500-545, main build).
- Failure source: scripted MCP runs above on both builds. In
McpPage.emulate() (src/McpPage.ts:1104-1110 on the main build, src/McpPage.ts:969-975 at the 1.10.1 tag), this.emulationSettings = Object.keys(newSettings).length ? newSettings : {} is assigned before await page.setViewport(newSettings.viewport ?? null) runs, and lighthouse_audit re-applies the stored settings through page.restoreEmulation() in its finally block (src/tools/lighthouse.ts:124-126).
- Evidence provenance: observed
- Local verification: reproduced
- Reproduction completeness: complete
Fix check
With the steps above, emulate {viewport:"800.5x600x1"} and emulate {viewport:"20000000x600x1"} either return a readable validation error or succeed with a value Chrome accepts; in no case does a later response print Emulating viewport with a width the page does not have, and both lighthouse_audit calls return a normal audit result. As controls, a valid emulate {viewport:"800x600x1"} still emulates an 800-pixel-wide page and is still listed in later responses, and the existing non-positive checks still reject 0x600.
Additional context
The full open and closed issue and PR corpus was screened as of 2026-10-06. Closed #2662, fixed by PR #2663, added the current positivity and finiteness checks in viewportTransform but not integer or upper-bound checks. Closed #2115 ("emulate is not atomic ...") reported the opposite desync: a throw from setViewport before the state was stored. Its fix, PR #2134, moved setViewport after the state commit and timeout update so that a viewport-triggered reload gets the throttled timeout; with that order, a setViewport call that Chrome rejects now leaves the rejected viewport stored. resize_page has a similar raw Browser.setContentsSize int32 error for fractional sizes; it is a different tool and code path.
Description of the bug
emulateaccepts aviewportwhose width or height is fractional (800.5x600x1) or above Chrome's limit (20000000x600x1). Chrome rejects the resultingEmulation.setDeviceMetricsOverridecall, and the tool returns a raw protocol error, but the rejected viewport has already been stored as the page's emulation state. From then on every response on that page printsEmulating viewport: {...,"width":800.5,...}although the page is not emulated, and everylighthouse_auditon the page runs the audit and then fails with the same protocol error, discarding the result. The state stays poisoned until the agent callsemulateagain without the bad viewport.Actual behavior
Both builds:
Reproduction
http://127.0.0.1:PORT/min.html, for example:tools/callrequests in order:{"name":"new_page","arguments":{"url":"http://127.0.0.1:PORT/min.html"}} {"name":"emulate","arguments":{"pageId":2,"viewport":"800.5x600x1"}} {"name":"evaluate_script","arguments":{"pageId":2,"function":"() => ({w: innerWidth, h: innerHeight, dpr: devicePixelRatio})"}} {"name":"take_screenshot","arguments":{"pageId":2}} {"name":"lighthouse_audit","arguments":{"pageId":2,"mode":"snapshot"}} {"name":"emulate","arguments":{"pageId":2,"viewport":"20000000x600x1"}} {"name":"lighthouse_audit","arguments":{"pageId":2,"mode":"snapshot"}} {"name":"emulate","arguments":{"pageId":2}} {"name":"lighthouse_audit","arguments":{"pageId":2,"mode":"snapshot"}}The sequence was run once per build with identical results; an earlier independent run on both builds gave the same results, including
lighthouse_auditin the defaultnavigationmode.Expectation
The
viewportparameter is documented as'<width>x<height>x<devicePixelRatio>[,mobile][,touch][,landscape]'(docs/tool-reference.md,emulate), and the server already validates it before use:viewportTransform(src/tools/ToolDefinition.ts:500-545at themainbuild) rejects non-positive or non-finite values with a readableInvalid viewport width ...message. A value Chrome cannot apply should either be rejected the same way, or, if it reaches Chrome and fails, leave the page's emulation state as it was, so that later responses report the emulation actually in effect andlighthouse_auditcan restore it.MCP configuration
--headless --isolated --no-usage-statistics --no-performance-cruxunless the reproduction states otherwise (update checks disabled withCHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS=1).Chrome DevTools MCP version
1.10.1 (npm) and
main5ddb0a3 (local build)Chrome version
154.0.8037.98 (stable)
Node version
v22.23.2
Operating system
macOS 27.0
Environment details
mainat commit 5ddb0a3 (themainbuild).--headless --isolated --no-usage-statistics --no-performance-cruxandCHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS=1, and driven by a scripted stdio MCP client.Evidence
emulate.viewportformat indocs/tool-reference.mdand the existing input validation inviewportTransform(src/tools/ToolDefinition.ts:500-545,mainbuild).McpPage.emulate()(src/McpPage.ts:1104-1110on themainbuild,src/McpPage.ts:969-975at the 1.10.1 tag),this.emulationSettings = Object.keys(newSettings).length ? newSettings : {}is assigned beforeawait page.setViewport(newSettings.viewport ?? null)runs, andlighthouse_auditre-applies the stored settings throughpage.restoreEmulation()in itsfinallyblock (src/tools/lighthouse.ts:124-126).Fix check
With the steps above,
emulate {viewport:"800.5x600x1"}andemulate {viewport:"20000000x600x1"}either return a readable validation error or succeed with a value Chrome accepts; in no case does a later response printEmulating viewportwith a width the page does not have, and bothlighthouse_auditcalls return a normal audit result. As controls, a validemulate {viewport:"800x600x1"}still emulates an 800-pixel-wide page and is still listed in later responses, and the existing non-positive checks still reject0x600.Additional context
The full open and closed issue and PR corpus was screened as of 2026-10-06. Closed #2662, fixed by PR #2663, added the current positivity and finiteness checks in
viewportTransformbut not integer or upper-bound checks. Closed #2115 ("emulate is not atomic ...") reported the opposite desync: a throw fromsetViewportbefore the state was stored. Its fix, PR #2134, movedsetViewportafter the state commit and timeout update so that a viewport-triggered reload gets the throttled timeout; with that order, asetViewportcall that Chrome rejects now leaves the rejected viewport stored.resize_pagehas a similar rawBrowser.setContentsSizeint32 error for fractional sizes; it is a different tool and code path.