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` 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.