mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-11-04 15:00:11 +00:00
The pre-submit checklist in `AGENTS.md` required every branch to be rebased onto `main`. A rebase without a conflict adds no information and forces a new revision on each `main` update. This change requires only that the branch has no merge conflicts with `main`. A rebase is required when a conflict exists.
12 KiB
12 KiB
This repository contains an MCP server and CLI for Chrome DevTools.
Instructions
- Use only scripts from
package.jsonto run commands. - Use
npm run buildto run tsc and test build. - Use
npm run testto build and run tests, run all tests to verify correctness. - Use
npm run test path-to-test.tsto build and run a single test file, for example,npm run test tests/McpContext.test.ts. - Use
npm run formatto fix formatting and get linting errors. - Never modify
third_party/devtools-frontendexcept for experimentation: it is a git submodule, a mirror of the actual codebase.
Rules for TypeScript
- Do not use
anytype. - Do not use
askeyword for type casting. - Do not use
!operator for type assertion. - Do not use
// @ts-ignorecomments. - Do not use
// @ts-nocheckcomments. - Do not use
// @ts-expect-errorcomments. - Prefer
for..ofinstead offorEach. - Never type-check types that are already type safe (e.g. redundant
typeofchecks on statically typed variables).
Rules for Testing
Structure and Separation of Concerns
- Prefer mock-based unit tests over real-browser tests: Do not use
withMcpContextor launch a real browser unless the test genuinely requires real browser or DevTools protocol integration (e.g., live CDP events, browser lifecycle, secondary sessions). Puppeteer already tests browser behavior upstream; unit tests run faster and avoid browser overhead. - Tool handler tests (
tests/tools/*.test.ts):- Test that the tool handler parses/validates parameters and invokes the corresponding methods on
page,context, orresponsewith the exact expected arguments. - Do not reimplement business logic or state tracking inside mocks (e.g., do not simulate state changes in mock methods).
- Test that the tool handler parses/validates parameters and invokes the corresponding methods on
- Core class tests (e.g.,
tests/McpPage.test.ts):- Test business logic by instantiating the real class under test with mocked dependencies (e.g., instantiate
new McpPage(...)with a mocked Puppeteer page fromcreateMockPuppeteerPage()). - Assert that the class calls the underlying Puppeteer methods with the expected parameters.
- Test business logic by instantiating the real class under test with mocked dependencies (e.g., instantiate
Mocking Guidelines (tests/mocks.ts)
- Centralize mocks in
tests/mocks.ts: Keep all reusable mock factories intests/mocks.ts. Import directly fromtests/mocks.ts(do not re-export fromtests/utils.ts). - Use
sinon.createStubInstance(Class): Do not hand-roll mock objects or define custom mock interfaces. Usesinon.createStubInstance()so all prototype methods are automatically stubbed. - Typing: Use
sinon.SinonStubbedInstance<Class>for mock types (e.g.,MockMcpPage,MockMcpContext,MockMcpResponse). - Handler mocks helper: For tool handlers, use
const {page, context, response} = createHandlerMocks();fromtests/mocks.tsto set up all three mocks in one call. - Keep mocks generic: Do not tailor mocks to a specific tool or test suite.
- Naming conventions: Use
mockrather thanfakein helper and variable names (e.g.,createMockPuppeteerPage,createMockMcpPage). Name the mocked Puppeteer page instancepptrPage.
Assertions and Sinon Best Practices
- Use
sinon.assertmethods: Do not use Node'sassert.ok(stub.calledOnce)orassert.deepStrictEqual(stub.firstCall.args[0], ...)to verify stub calls. - Verify exact arguments:
- Use
sinon.assert.calledOnceWithExactly(stub, ...args)for single calls with exact arguments. - Use
sinon.assert.calledWithExactly(stub.secondCall, ...args)for subsequent calls. - Use
sinon.assert.notCalled(stub)to assert a method was not invoked.
- Use
- Clean up stubs:
tests/setup.tsrunssinon.restore()after every test. Do not addafterEach(() => sinon.restore());to test suites.
Test Cleanliness
- Do not add redundant comments or verbose JSDoc for short, self-describing mock functions or tests.
- Only test real scenarios; avoid testing redundant or artificial calls that cannot happen in real usage.
- Use current year (2026) in copyright headers for new test files.
Pre-submit checklist
Complete every item before you upload a PR or push a new revision.
Scope and PR description
- The PR contains one change. Move unrelated refactors, renames, formatting changes, and build or compiler changes into their own PR.
- The PR title, the commit type (
fix:,feat:,chore:), and the squash commit message describe the same change. A change to a default value or to tool output is afix:or afeat:, not achore:. - The PR description lists every user-visible behavior change, including changes to other tools that share the changed code path.
- The PR description matches the shipped behavior. Remove claims about fixes that the diff does not contain.
- The PR description names new functions that have no production caller yet, or the PR removes them.
- The branch has no merge conflicts with
main. Rebase only when a conflict exists. npm run genran after a change to a tool schema, a tool description, or a CLI option. The generated files are committed.npm run formatran. No line is longer than 80 columns.npm run testpassed locally. CI failures on all platforms are your responsibility.
Design
- The change lives in the lowest layer that owns the data: page logic in
McpPage, context logic inMcpContext, formatting insrc/formatters/*.McpResponsedoes not resolve pages or workers. - If Puppeteer or devtools-frontend owns the bug, fix it there first. Do not copy or re-implement upstream code in this repository.
- Puppeteer internals are imported only through
src/third_party/index.ts. - Repeated code is one helper. Extract a helper when the same block
appears two or more times. Prefer an existing helper or a Puppeteer
utility (for example
Deferred) over a new one. - A new formatter class exposes
toJSON()andtoString(), andtoString()uses thetoJSON()output. - A function with three or more optional parameters takes one options
object. Do not pass positional
undefinedplaceholders. - Each default value is declared in one place.
- A new behavior is opt-in by default when a wrong classification fails silently (for example a config option is restart-required unless the author marks it reloadable).
- An invalid combination of flags or options is rejected by the type system where possible, not only by a comment.
- No public method exists only to serve a test. Put shared helpers in
src/utils/*. - Parameter validation lives in the Zod schema (
refine,coerce), not in the handler. Validation runs before the handler changes any state. - Numeric validation matches what the consumer accepts. Use
Number.isIntegerand a range check where CDP expects integers.
Correctness and edge cases
- A failed external call does not leave stale state. Restore the previous value when a call rejects.
- An error path still produces the artifacts and partial results the agent needs before it throws.
- An async operation that a timeout can abandon checks that its target (browser, page, config) is still current when it resumes.
- A lock or guard is acquired and released exactly once. A helper that releases a guard throws when the guard is already released.
- Concurrent callbacks (watchers, reloads, event handlers) are serialized. A transient failure does not trigger a callback.
- A filesystem operation that races with another process tolerates the
race (
fs.rmSync(path, {force: true})ortry/catch), notexistsSync+ an unguarded call. - A failed lookup falls back to the previous behavior. It does not turn a healthy state into an error state.
- No synchronous process spawn or blocking call runs in a polling loop or in a hot function.
dispose()releases every resource the object created, and runs for objects that are already closed.- Event listeners added for one action are removed when the action ends.
- Cached page-side state is refreshed before use. The page can reload between tool calls.
- Behavior shared by all tools that act on the selected page also runs
for tools defined with
defineTool, not onlydefinePageTool. - String and URL parsing handles userinfo, escaped delimiters, ports, pattern groups, and empty or whitespace-only input. Use the full authority, not a heuristic on the first or last character.
- Truncation uses
Intl.Segmentergraphemes, notstring.slice. - Secrets never reach logs. Redact headers, URL userinfo, and query strings in every log line, including process startup arguments.
- A filesystem path parameter is validated with
validatePath()even when the value parses as a URL. The error says what the parameter expects. - A value that crosses a process boundary (daemon IPC, CLI) is
serialized with
JSON.stringifywhen it is not a primitive. - An identifier is issued only for items that a tool can resolve later.
- A dedup key never collapses for items without identifying fields. Use a counter or skip such items.
- An error message names the exact condition that failed, with the relevant values (pid, timeout, URL, parameter name).
- A thrown non-
Errorvalue keeps its content in the error message. Treat objects with a stringmessagelikeError. - Optional chaining and
?? defaultare not applied to values whose type is non-optional. - Version-dependent behavior (Node, Chrome, OS) is tested on every
version the
enginesfield allows, or the test does not depend on it.
Tests
- Every new or changed function has a test that calls it. A function
with no caller in
src/and no test is removed from the PR. - Every review request of the form "add a test for X" has a test whose name includes X.
- A new test for a bug fails on
mainwithout the fix. Remove assertions that pass with and without the fix. - A
notCalledassertion targets the first method the handler calls after the step under test, not a method inside a callback that a stub never runs. - A test for a selected branch also asserts that the other branch was not taken.
- Assertions use
calledOnceWithExactlyorcalledWithExactly(stub.nthCall, ...). UsecalledOnceWithMatchonly for arguments that cannot be exact (for example anAbortSignal) or when an overloaded signature forcessinon.match.same. - Options that the handler derives from its input are asserted, not only the call count.
- Test names describe the asserted behavior. Rename the test when the assertion changes.
if (value) { assert... }does not appear in tests. Useassert.ok(value)before the dependent assertions.- Real-browser tests exist only for behavior that mocks cannot observe. A test file contains at most one slow end-to-end run per scenario.
- A real-browser test that counts requests filters
favicon.ico. - A real-browser test that waits for CDP events waits on the session or
result of the code under test, not on a new
createCDPSession(). - Every awaited event in a test has a bounded timeout. Positive-path tests poll for the condition instead of a fixed sleep.
- New reusable stubs and fixtures live in
tests/mocks.tsand build onsinon.createStubInstance. No hand-rolled fixture usesas unknown as. - Mock options that equal the default value are not passed.
- Temporary directories use
using dir = createTempDir(...)fromtests/utils.ts, notfs.mkdtemp. - A test never reads the developer's home directory and never calls
process.exit. Stub locators and passexitProcess = false. - When a real-browser test is replaced with mocks, each branch the old test covered still has a test.
- A test does not repeat a round-trip that another test already covers. Stop after the first assertion that proves the change.
Documentation and output
- A tool description states the exact behavior, including when a parameter is ignored or depends on another parameter.
- Skills stay generic. Add one short note in the tool-selection section instead of a feature-specific workflow.
- Inline comments are one sentence. Remove comments that repeat an existing TSDoc.
- Response text that depends on a condition prints only when the condition is true.