Files
chrome-devtools-mcp/AGENTS.md
Alex Rudenko d1143acbfc docs: require a rebase only when the branch has merge conflicts (#3239)
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.
2026-10-19 11:08:13 +00:00

12 KiB

This repository contains an MCP server and CLI for Chrome DevTools.

Instructions

  • Use only scripts from package.json to run commands.
  • Use npm run build to run tsc and test build.
  • Use npm run test to build and run tests, run all tests to verify correctness.
  • Use npm run test path-to-test.ts to build and run a single test file, for example, npm run test tests/McpContext.test.ts.
  • Use npm run format to fix formatting and get linting errors.
  • Never modify third_party/devtools-frontend except for experimentation: it is a git submodule, a mirror of the actual codebase.

Rules for TypeScript

  • Do not use any type.
  • Do not use as keyword for type casting.
  • Do not use ! operator for type assertion.
  • Do not use // @ts-ignore comments.
  • Do not use // @ts-nocheck comments.
  • Do not use // @ts-expect-error comments.
  • Prefer for..of instead of forEach.
  • Never type-check types that are already type safe (e.g. redundant typeof checks on statically typed variables).

Rules for Testing

Structure and Separation of Concerns

  • Prefer mock-based unit tests over real-browser tests: Do not use withMcpContext or 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, or response with the exact expected arguments.
    • Do not reimplement business logic or state tracking inside mocks (e.g., do not simulate state changes in mock methods).
  • 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 from createMockPuppeteerPage()).
    • Assert that the class calls the underlying Puppeteer methods with the expected parameters.

Mocking Guidelines (tests/mocks.ts)

  • Centralize mocks in tests/mocks.ts: Keep all reusable mock factories in tests/mocks.ts. Import directly from tests/mocks.ts (do not re-export from tests/utils.ts).
  • Use sinon.createStubInstance(Class): Do not hand-roll mock objects or define custom mock interfaces. Use sinon.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(); from tests/mocks.ts to 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 mock rather than fake in helper and variable names (e.g., createMockPuppeteerPage, createMockMcpPage). Name the mocked Puppeteer page instance pptrPage.

Assertions and Sinon Best Practices

  • Use sinon.assert methods: Do not use Node's assert.ok(stub.calledOnce) or assert.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.
  • Clean up stubs: tests/setup.ts runs sinon.restore() after every test. Do not add afterEach(() => 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 a fix: or a feat:, not a chore:.
  • 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 gen ran after a change to a tool schema, a tool description, or a CLI option. The generated files are committed.
  • npm run format ran. No line is longer than 80 columns.
  • npm run test passed 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 in McpContext, formatting in src/formatters/*. McpResponse does 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() and toString(), and toString() uses the toJSON() output.
  • A function with three or more optional parameters takes one options object. Do not pass positional undefined placeholders.
  • 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.isInteger and 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}) or try/catch), not existsSync + 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 only definePageTool.
  • 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.Segmenter graphemes, not string.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.stringify when 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-Error value keeps its content in the error message. Treat objects with a string message like Error.
  • Optional chaining and ?? default are not applied to values whose type is non-optional.
  • Version-dependent behavior (Node, Chrome, OS) is tested on every version the engines field 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 main without the fix. Remove assertions that pass with and without the fix.
  • A notCalled assertion 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 calledOnceWithExactly or calledWithExactly(stub.nthCall, ...). Use calledOnceWithMatch only for arguments that cannot be exact (for example an AbortSignal) or when an overloaded signature forces sinon.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. Use assert.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.ts and build on sinon.createStubInstance. No hand-rolled fixture uses as unknown as.
  • Mock options that equal the default value are not passed.
  • Temporary directories use using dir = createTempDir(...) from tests/utils.ts, not fs.mkdtemp.
  • A test never reads the developer's home directory and never calls process.exit. Stub locators and pass exitProcess = 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.