Skip to main content
Glama
CDP-MCP Server

CDP-MCP Server

by jekyll-001

CDP-MCP Server

A system-wide MCP server that wraps the Chrome DevTools Protocol, giving AI agents full access to low-level browser debugging: breakpoints, stack traces, stepping, scope inspection, network interception, and source maps.

72 MCP Tools

Category

Count

Highlights

Connection

5

Connect, disconnect, list/attach targets, raw CDP commands

Debugger

19

Breakpoints, valid-position lookup, range blackboxing, non-pausing logpoints, stepping, call stacks with async parents, scope inspection, eval on frame, exception breakpoints, blackboxing

Runtime

5

JS evaluation, object property drilling, function → source location, console capture

Network

11

Request monitoring with initiator stacks, response bodies, body search, WebSocket/SSE frames, Fetch interception (continue/fail/fulfill)

Source

6

Script listing, source retrieval, ranged excerpts, search with coordinates, source maps in both directions

DOM / events

4

Break on DOM change, on request URL, on event type; list event listeners with locations

Coverage

2

Which functions ran during one action, with call counts

Frameworks

10

Detect; React and Vue component chains with state; React context map; external store detection; Redux state and action tracing; Next.js data; webpack module search

Data flow

8

One timeline of actions, commits, store changes, writes and calls; value finder; property write watch; function argument/return tracing; render tracing; vendor detection

Trace

2

Cursor-based timeline and UI action markers

Related MCP server: MCP JS Debugger

Minified bundles and data flow

A production React, Next.js or Vue chunk is usually one very long line, so work by position rather than by file:

  1. source_search: {"query":"saveProfile","urlFilter":"chunk"} — each match has a 1-based line, 0-based column and surrounding text.

  2. source_get_excerpt: read a window around that position instead of the whole file.

  3. runtime_function_location: {"expression":"getEventListeners($0).click[0].listener"} — go from a live function (handler, component, store action) to its definition.

  4. debugger_set_logpoint: {"urlRegex":"chunk.*\\.js$","lineNumber":1,"columnNumber":48213,"expression":"({id: e.id, next: t})"} — records the value on every pass without pausing; read with debugger_get_logpoint_hits.

  5. network_get_requests: each request carries initiator with the JS stack that sent it.

  6. network_search_bodies: {"query":"ord_777"} — find which response, request body, WebSocket frame or SSE message carried a value, then follow its requestId back to the initiator. network_get_stream_frames lists the socket and event-stream traffic.

Body search reads bodies from Chrome on demand, so it only covers the latest 500 requests and bodies Chrome still holds. A response the page never read, one still loading, or one Chrome has evicted is counted in bodiesUnavailable; binary responses and binary frames are skipped. The stream buffer keeps the latest 1,000 frames per attachment, each capped at 100,000 characters.

When execution does pause, debugger_get_pause_info includes asyncStack: the promise, timer or event chain that scheduled the paused code. Logpoint values are JSON, truncated to 2,000 characters, in a 1,000-entry buffer per attachment; an expression or condition that is not valid JavaScript is rejected when the logpoint is set.

Finding the code behind an action

  • dom_get_event_listeners lists the handlers on an element with their locations; pass includeAncestors for frameworks that delegate events to the root.

  • coverage_start, replay one action, coverage_take: the functions that ran, with call counts and positions. Counts reset on each take.

  • xhr_breakpoint, dom_breakpoint and event_breakpoint pause in the code that sends a request, changes a node or handles an event. The pause's reason is XHR, DOM or EventListener and data carries the engine's details.

  • debugger_get_possible_breakpoints lists where the engine can pause near a position; debugger_blackbox_ranges skips vendor code that shares a chunk with application code.

Source maps

Parsed maps are cached per attachment. source_resolve_sourcemap goes from a generated position to the original, source_find_generated_position from an original file and line to the running bundle, and debugger_get_call_stack with resolveSourceMaps: true adds original positions to each frame. Map URLs resolve the way a browser resolves them (absolute, root-relative and relative). Production sites often publish no maps; the tools above work without them.

Data flow without source maps

Everything below works on minified production builds. Tracers report into one timeline kept in the server, read with flow_get_events; it holds the latest 2,000 events per attachment and survives page navigations. Replay a UI action, then read the timeline to see the chain: action dispatched → state paths changed → components re-rendered and why.

Event kind

Source

Contents

redux

redux_trace

Action type and payload, changed state paths, dispatching call stack

react-commit

react_trace_renders

Components that re-rendered and why: changed props, hook indexes, context IDs, or parent only

store

store_trace

Changed paths and snapshot of a non-Redux store, with the stack of the code that changed it

write

watch_property

New and previous value of a watched property, with the writer's stack

logpoint

debugger_set_logpoint, debugger_trace_function

Recorded expression, or a traced function's arguments (call:<label>)

return

debugger_trace_function with captureReturn

The function's return value

Every stack frame and component location is a generated position (1-based line, 0-based column) that source_get_excerpt and debugger_set_logpoint accept.

Redux. redux_inspect finds the store through react-redux's Provider and reads state at a dot path. redux_trace start reads the closure that store.getState shares with Redux's internal dispatch and hooks that function with a non-pausing breakpoint, so actions dispatched from thunks, sagas and listener middleware are captured as well as direct ones; the result says "capture":"reducer" when this worked and "store.dispatch" when it fell back to wrapping the public method. After a navigation the tracer re-attaches by itself; actions during page startup are recorded with their stack but without a state diff.

Other stores. react_find_stores finds anything components subscribe to through useSyncExternalStore — Zustand, Jotai, MobX bindings, modern react-redux — and reports the subscribed components and where the store's subscribe is defined. store_trace records its changes. Store IDs are per page load.

Context. Production builds strip context names, so each context gets a stable ID (ctx3) and a label from the shape of its value ({theme, setTheme}). react_context_map lists every context with its providers, the component that computes each provided value and where it is defined, and the components that read it. react_inspect shows the same IDs per component, and react-commit events name the context that caused a re-render.

Renders. react_trace_renders installs the hook React reports commits to. React only registers with a hook that exists before it loads, so the first start usually needs reload: true. Locate a reported component with runtime_function_location and the expression __cdpMcpReact.component(<id>).

One value, end to end. flow_find_value reports everywhere a value currently is: network bodies and frames, Redux paths, external stores, component props, hooks and class state, context values, DOM nodes and web storage.

Outside any store. watch_property records assignments to one property of a live object, including DOM properties such as an input's value. debugger_trace_function records a function's arguments on each call, and with captureReturn its return value.

Vendor code. source_identify_vendors recognizes libraries by string literals that survive minification. On webpack builds it resolves to module ranges and blackbox: true makes stepping skip exactly those; on other bundlers it reports libraries per script.

Limits:

  • These tools read React, Redux and bundler internals and place hooks by matching minified code shapes. A new major version can break one of them while the page itself is fine.

  • The Redux hook needs Redux's own createStore closure. A store with a different implementation falls back to store.dispatch wrapping, which sees the state change and stack of a middleware-internal dispatch but not its action object.

  • captureReturn pauses the page for an instant at every return of the traced function; avoid it on hot functions. Argument capture never pauses. Destructured parameters of arrow functions are not captured.

  • watch_property sees assignments to the property, not mutation inside its value, and cannot watch non-configurable properties.

  • react_trace_renders reports at most 30 components per commit and 25 commits per second; the rest are counted, not listed.

  • Tracers change the page: they wrap store.dispatch, redefine watched properties and set debugger breakpoints. stop/untrace restore each one; a detach removes the breakpoints.

Framework helpers

framework_detect, react_inspect, vue_inspect, next_get_data and webpack_find_modules read framework internals from the page: React fibers, Vue 2/3 component instances with Pinia/Vuex state, __NEXT_DATA__ and the App Router flight payload, and the webpack module registry. Component and module results include definedAt, a position usable for breakpoints and logpoints. These internals are not public API and change between versions, so a helper can fail on a page that is otherwise fine. The automated tests exercise them against stand-in objects shaped like each framework's internals; they were also checked by hand against production builds of React 19 with the Next.js Pages Router and webpack, Vue 3.5, and the Next.js App Router with Turbopack, and a production Redux Toolkit app that also uses Zustand. Vue 2, Pinia/Vuex, webpack 4, Jotai, MobX and redux-saga are covered by neither. Turbopack's registry is not supported.

source_search and coverage_take leave out browser-extension content scripts unless a urlFilter or scriptId selects them. webpack_find_modules pushes one empty chunk into the page's chunk array to obtain the require function.

Tracing lazy-loaded SPAs

Connecting or attaching installs script listeners before enabling Debugger, so source_list_scripts includes existing scripts immediately. Set a URL or URL-regex breakpoint before replaying an action to catch a chunk that has not loaded yet. debugger_set_breakpoint returns locations from CDP; an empty array means the breakpoint has not resolved yet. Later breakpointResolved events update debugger_list_breakpoints and appear in the timeline.

All public generated coordinates use 1-based lines and 0-based columns, including breakpoint inputs, resolved locations, call stacks, script inventory, source-map inputs, and trace events. Column defaults to 0. This changes the old breakpoint column convention (previously documented as 1-based) and the inventory's startLine/endLine convention. Raw cdp_send_command requests and replies retain native CDP coordinates (both 0-based).

Every tool's text JSON and structuredContent use this envelope:

{
  "targetId": "chrome-target-id",
  "sessionId": "attachment-uuid",
  "data": { "paused": false, "callFrames": [] }
}

Clients should read the former result from data. sessionId is a server-generated attachment UUID, not a flattened CDP protocol session ID. Reattaching to the same target generates a new UUID. Disconnected results have null IDs. Disconnect, target switch, failed attachment, and unexpected socket loss clear manager state, subscriptions, breakpoints, pause state, scripts, console/network buffers, timers, and the timeline. Old asynchronous callbacks cannot populate a replacement session. Pending steps reject on session loss; commands spanning a switch return an error rather than evidence labeled with the new target.

Automatic reattachment

If the Chrome connection is missing when a tool is called — the socket dropped, or the MCP host restarted this server — the server attaches again before running the tool. After a drop it returns to the same target; in a fresh server process it behaves like cdp_connect. The envelope of that call carries "reconnected": true and a notice, because the new session starts with no breakpoints, interception, buffers or timeline. It never moves to a different tab by itself: if the previous target is gone the call fails and asks for cdp_list_targets/cdp_attach_target. After an explicit cdp_disconnect nothing reattaches until cdp_connect or cdp_attach_target is called. cdp_connect skips DevTools windows when choosing the first page.

Example MCP tool sequence (arguments shown as JSON):

  1. cdp_attach_target: {"targetId":"..."}

  2. debugger_set_breakpoint: {"urlRegex":"lazy-chunk.*\\.js$","lineNumber":1,"columnNumber":86}

  3. trace_mark: {"label":"click Search"} — save data.cursor.

  4. Replay the UI action, then inspect pause info, resolved breakpoints, and scopes.

  5. trace_get_events: {"cursor":123,"limit":100} — use the mark's cursor minus one to include the mark, or the mark's cursor to read events after it.

  6. Continue with the returned data.nextCursor while data.hasMore is true.

The timeline retains at most 1,000 events per attachment: scriptParsed, breakpointResolved, paused, resumed, script network requests (scriptRequest), and mark. Cursors are exclusive, strictly increasing within one MCP server instance, and are not reused after switching targets. truncated signals that events after the supplied cursor were evicted or cleared on session reset; oldestCursor identifies the oldest retained event. Persist events externally before switching targets if you need a cross-tab investigation history.

Each event carries target/session IDs and a Unix-millisecond receipt timestamp. Use cursor order for arrival order; this is not a claim about browser causal timing. URLs, script IDs, and generated coordinates are included when CDP makes them available. Script network requests carry request ID, URL, and method; CDP does not supply a script ID or generated location at that stage, so correlate with subsequent scriptParsed events by URL. Resume events carry the preceding pause's top location. Initial script events replayed by Debugger.enable describe inventory, not new loads.

The trace records no request/response bodies, cookies, authorization headers, other headers, console values, or scope values. It does retain URLs and explicit marker labels. The existing explicit network and scope inspection tools retain their behavior. Step over/into/out wait for a new pause event and command reply, with a 5-second timeout; a timeout is returned as an MCP tool error.

Tests

npm run build
npm test                 # build, regression tests, and real Chromium acceptance
npm run test:unit        # regression tests only
npm run test:acceptance  # real Chromium two-tab fixture only

The acceptance test requires Chromium (/usr/bin/chromium by default; override with CHROMIUM_PATH) and permission to bind loopback ports and launch a headless browser. It uses a temporary browser profile and a local HTTP fixture, verifies a pre-load breakpoint in a single-line dynamically imported chunk, local/closure scopes, stepping, ordered trace events, and isolation across target switches, reattachment, disconnect, failed attachment, and target closure. MCP tool calls use the SDK's in-memory transport and a real CDP connection. No account or external website is involved.

Quick Start

# Install
npm install

# Build
npm run build

# Launch Chromium with debugging
chromium --remote-debugging-port=9222

# Run (stdio mode for single agent)
node dist/bin/cdp-mcp-server.js --stdio

# Run (HTTP mode for multi-agent)
node dist/bin/cdp-mcp-server.js --http --port 3100

In HTTP mode each MCP session (one per initialize) gets its own server and Chrome attachment, addressed by the mcp-session-id header on POST, GET and DELETE /mcp. DELETE ends a session; sessions whose client vanishes stay open until the server stops. GET /health reports the number of active sessions.

MCP Client Config

{
  "mcpServers": {
    "cdp-debugger": {
      "command": "node",
      "args": ["/path/to/cdp-mcp-server/dist/bin/cdp-mcp-server.js", "--stdio"]
    }
  }
}

CLI Options

--stdio              Stdio transport (default)
--http               HTTP transport
--port <n>           HTTP port (default: 3100)
--chrome-port <n>    Chrome debug port (default: 9222)
--chrome-path <p>    Chrome binary (default: /usr/bin/chromium)
--profile isolated   Temp profile (default)
--profile real       Use real profile
--attach             Only attach, never launch
--launch             Always launch new instance

Architecture

  • Built from scratch — No forks, full control

  • Browser page debugging — Not Node.js-only like alternatives

  • Never kills your Chrome — Detaches on exit; only a browser this server launched itself is terminated

  • Network interception safety — 30s auto-continue watchdog, emergency cleanup on crash

  • Dual transport — stdio for single-agent, HTTP for multi-agent

  • Guarded raw access — cdp_send_command refuses the few commands that would desynchronize the server (Debugger/Runtime/Network.disable, Fetch.enable/disable)

License

MIT

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI assistants to debug Node.js applications using Chrome DevTools Protocol. Provides comprehensive debugging capabilities including breakpoints, stepping, variable inspection, expression evaluation, and console monitoring.
      5,737 npm
      350
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      Enables AI assistants to debug JavaScript and TypeScript applications by connecting to Chrome DevTools Protocol-compatible debuggers, allowing them to set breakpoints, step through code, inspect variables, and evaluate expressions with full source map support.
      18
      10 npm
      2
      Apache 2.0
    • -
      license
      Not graded
      quality
      Not graded
      maintenance
      Enables AI coding assistants to control and inspect a live Chrome browser for automated debugging, performance analysis, and web interaction. It leverages Puppeteer and Chrome DevTools to provide capabilities like network monitoring, console logging, and automated browser actions.
      -
    • A
      license
      B
      quality
      B
      maintenance
      Enables AI agents to debug code and automate browsers using Chrome DevTools Protocol, supporting breakpoints, variable inspection, and replayable interaction recording.
      35
      548 npm
      19
      MIT