2026-07-15
Chrome DevTools MCP: The Server Was Alive, Chrome Was Not
A Chrome DevTools MCP 1.6.0 fixture shows why server health is not browser health, and how to bind snapshots, console and network evidence into a reproducible debugging packet.
The MCP server was alive. Chrome was not. In a local Chrome DevTools MCP 1.6.0 run, the protocol handshake completed and the server returned all 39 of its tools. The first browser call then failed with Target closed. That split result is the most useful fact in this article: a green MCP connection is not evidence that a coding agent can observe a page.
I kept the failed attempt instead of editing it out of the story. After a bounded recovery for this root-owned disposable CI fixture, the same probe opened a synthetic checkout page, resized it to 390 by 844 pixels, captured the semantic page snapshot, triggered one failed request, and read both console and network records. No application was patched. The artifact tests the debugging loop itself: can another engineer reconstruct what the browser saw, what action occurred, and why the run failed?
Package chrome-devtools-mcp@1.6.0, pinned for the run
Environment Node 22.23.1 and Chrome 148.0.7778.215
Protocol requested and negotiated 2025-06-18
Fixture signal one warning on load, one failed fetch after submit, one handled application error
Evidence boundary one local headless fixture, not a performance, memory, security, or interoperability benchmark
Failure modes of a green server
MCP initialization answers a narrow question: can the client and server agree on a protocol and exchange messages? Tool enumeration answers another: what contract is being advertised? Neither call needs a working inspected page. The official README makes the same lifecycle distinction in a quieter way: Chrome starts only when a tool requires a browser.
That is why the first run was not contradictory. The probe negotiated the protocol with server version 1.6.0, and tools/list exposed 39 names. The subsequent new_page call crossed a new boundary—starting Chrome—and returned Protocol error (Target.setDiscoverTargets): Target closed. The project’s troubleshooting guide defines that family of error as Chrome failing to start or exiting. Retrying protocol initialization would have tested the part that had already worked.
The test environment runs as root inside a disposable CI container. Chrome refuses its normal launch there unless the environment supplies an appropriate sandbox arrangement. For this one retained fixture, I added --chrome-arg=--no-sandbox and --chrome-arg=--disable-setuid-sandbox, reran the exact probe, and recorded those flags in the result. That is a recovery note, not a setup recommendation.
--browser-url. Moving the browser boundary is safer than deleting every boundary around it.
A shared or scheduled worker should also pin the package under test. The convenient starter configuration uses @latest; a reproduced incident needs a version. This was the relevant launch shape after removing the CI-only flags:
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": [
"-y",
"chrome-devtools-mcp@1.6.0",
"--headless",
"--isolated",
"--no-usage-statistics",
"--no-performance-crux",
"--memoryDebugging=true"
]
}
}
}
--isolated matters here because it creates a temporary user-data directory and removes it after the browser closes. It prevents a scheduled test from quietly inheriting cookies and tabs from a previous incident. It does not make a sensitive account safe to expose to the MCP client.
Six calls make one reviewable incident
The successful rerun was deliberately small. The fixture had a heading, a submit button, a live status field, a warning emitted at load, and a fetch to https://example.invalid/shipping after the click. The request was supposed to fail. The question was whether the browser evidence would preserve that designed causal chain.
1. Open the case
new_page selected the synthetic checkout fixture and returned the page list.
2. Bind the viewport
resize_page set 390 by 844 before any diagnosis.
3. Preserve pre-action state
take_snapshot recorded the heading, button, and idle status.
4. Trigger one action
evaluate_script clicked submit and immediately returned status working.
5. Read console evidence
list_console_messages returned one warning and two errors after the request settled.
6. Read request evidence
list_network_requests returned the single failed fetch and its transport reason.
The order is part of the result. The snapshot says the page was idle before the action. The script result says it was working immediately after the click. The later console entry says the application handled a TypeError. Without that sequence, a transcript can contain all the right facts and still attribute them to the wrong state.
Console and network disagree usefully
The fixture emitted one application warning: fixture-ready. After submit, Chrome recorded Failed to load resource: net::ERR_NAME_NOT_RESOLVED, and the application’s catch block recorded shipping-failed TypeError. The network list contained one request: the expected shipping URL with net::ERR_NAME_NOT_RESOLVED.
Those are not duplicate observations. The network record identifies the request and transport outcome. The browser-generated console error shows how Chrome surfaced the failure. The application error shows that its own handler ran and reduced the exception to a TypeError. A network-only review could miss a broken catch path. A console-only review could leave the failed URL and request identity implicit.
| Claim | Best evidence | What it does not prove |
|---|---|---|
| The intended control existed before the action | Semantic snapshot with selected page and pre-action status | That the handler was attached correctly |
| The requested viewport was active | Runtime readback of [390,844] | That every responsive breakpoint is sound |
| The application attempted the shipping fetch | Network request URL and failure result | That the response handler rendered the right state |
| The catch path executed | Application-authored console error | That the user received a useful recovery message |
| The whole bug is fixed | A matched after-run plus an adjacent regression check | Anything, if only the before-run exists |
This is the practical difference between browser control and browser debugging. Clicking is an action. A claim about the click requires state before it, runtime evidence after it, and a rule for deciding whether the result is acceptable.
Validation means repeating the case, not the click
A coding agent can make a plausible patch and then accidentally test a different case: another tab, a warm cache, a desktop viewport, a different package version, or a later page state. The after-run may look healthier because the experiment drifted.
I would bind an after-run to at least seven fields: package version, Chrome version, target origin, selected page identity, viewport and emulation, reproduction steps, and evidence filters. Then I would repeat the same snapshot point, the same action, and the same console and network queries. Timestamps or monotonic sequence numbers make the packet easier to audit when an asynchronous error arrives after the action result.
The acceptance rule should be stated before editing. For this fixture, a successful repair would not mean “no TypeError.” It would mean the request goes to the intended test endpoint, the expected request count is one, the response has an accepted status, the status field reaches the intended terminal value, and no new application error appears. Removing the catch block could silence one message while making the page worse.
Only after that matched run passes should the agent broaden once: one adjacent viewport, one neighboring route, or one appropriate Lighthouse or performance check. Broad collection before a hypothesis tends to produce a large folder and a weak diagnosis. Broadening after the core case guards against the most obvious regression.
Page identity is evidence
Version 1.6.0 included several fixes that sound like housekeeping until a coding agent is juggling tabs. The release keeps a selected page when it is still open instead of falling back to the first page, keeps page IDs unique across browser reconnects, resolves IDs only among listed pages, and reports when fallback replacement occurs. It also fixed first-page pagination for console and network listings.
These details affect whether an evidence packet describes the intended page. Record the page list and selected identity at the start. Record it again after a navigation, reconnect, dialog, or target close. If the server reports a fallback, stop and rebind the case before interpreting old console messages as evidence about the new page.
Concurrent agents make that obligation sharper. The project documents --experimentalPageIdRouting for a server shared across agents, and --isolated for independent sessions that need separate temporary profiles. Neither flag assigns ownership by itself. The orchestration layer still needs to decide which worker owns which target and to prevent two workers from diagnosing the same tab under different assumptions.
The tempting escape hatch is not a sandbox
Our probe did not advertise the MCP roots capability. The server printed a useful warning: file-writing tools would be restricted to the operating system’s temporary directory. It also mentioned --allow-unrestricted-paths as a way to restore the previous broad behavior. The warning is an admission record, not an invitation.
The project’s security policy is explicit about the boundary. MCP roots can constrain file access for operations that honor them, but they are optional and the temporary directory remains accessible. The maintainers recommend an operating-system sandbox when full filesystem isolation matters. Similarly, allowed and blocked URL patterns are guardrails for attached Chrome targets, not a complete network sandbox.
That yields a less convenient but more honest setup:
- Use a dedicated browser profile or an isolated temporary profile; do not expose personal tabs.
- Keep production credentials out of the test unless the incident specifically requires a supervised authenticated case.
- Negotiate the narrow roots the workflow needs and retain the server’s boundary warning in the run record.
- Use OS or VM isolation for filesystem and network guarantees that the MCP server does not claim to provide.
- Keep
--allow-unrestricted-pathsoff unless a trusted local client has a documented need and an external sandbox supplies the missing boundary.
Browser visibility also does not grant business authority. A tool can inspect a purchase flow without being allowed to submit payment. It can diagnose an admin page without being allowed to change permissions. The evidence packet should name those stop lines before the first click.
Memory tools deserve a different case file
With --memoryDebugging=true, the 1.6.0 server advertised eleven heap tools in this run, including snapshot comparison, duplicate strings, dominators, retainers, and retaining paths. The release also added aggregate filters plus object counts and total sizes in snapshot details.
I did not take a heap snapshot, so this article does not claim a memory result. That negative statement matters. Tool availability is not evidence that a leak exists, and one larger snapshot is not evidence of a leak either. A defensible memory case needs matched warm-up, navigation count, snapshot points, garbage-collection policy, and a retaining path that explains monotonic growth across repeated cycles. It should be a separate packet because its timing and resource controls are different from a failed fetch.
The same restraint applies to Lighthouse and performance traces. They are useful when the acceptance rule concerns accessibility, SEO, or runtime cost. Running every diagnostic surface after every click dilutes the incident record and may send a trace URL to CrUX unless that integration is disabled. Collect the artifact that can falsify the current hypothesis.
Checklist for the pull request
A reviewer should not have to replay an agent transcript to learn what changed. The smallest useful packet is compact enough to read and specific enough to reproduce:
Case identity
Target, package and Chrome versions, selected page, viewport, profile mode, and run timestamp.
Observed failure
Exact reproduction step plus the snapshot, console item, request, trace insight, or retaining path that supports it.
One hypothesis
A falsifiable link from the evidence to a named source or lifecycle path—not a list of possible fixes.
Focused change
Files changed, relevant code-level test, diff summary, and behavior deliberately left untouched.
Matched verification
The same measurement before and after, including request count and user-visible terminal state.
Boundary notes
Credentials, roots, URL policy, sandbox arrangement, external side effects, and approvals that were absent or held.
The retained probe for this article can defend a modest claim: Chrome DevTools MCP 1.6.0 exposed the expected diagnostic surface, and after one recorded browser-start recovery it bound semantic state, runtime action, console output, and network failure to one selected mobile-sized page. It cannot defend a claim about a repaired application because none was repaired.
That is the browser debugging loop worth automating. Make the environment failure visible. Bind evidence to one case. Change one responsible unit. Repeat the same observation. If the packet cannot tell another engineer which page failed and why, more autonomous clicking will not rescue it.
Primary sources: the official Chrome DevTools MCP 1.6.0 documentation, v1.6.0 release notes, security policy, and troubleshooting guide. The local protocol fixture and its result are retained with this remediation batch.