repo_description: Chrome DevTools for coding agents
repo_topics: browser chrome chrome-devtools debugging devtools mcp mcp-server puppeteer
code: /**
   * @license
   * Copyright 2025 Google LLC
   * SPDX-License-Identifier: Apache-2.0
   */
  
  import type * as puppeteer from './third_party/index.js';
  import type {DevTools} from './third_party/index.js';
  import {CDPSessionEvent} from './third_party/index.js';
  
  /**
   * This class makes a puppeteer connection look like DevTools CDPConnection.
   *
   * Since we connect "root" DevTools targets to specific pages, we scope everything to a puppeteer CDP session.
   *
   * We don't have to recursively listen for 'sessionattached' as the "root" CDP session sees all child session attached
   * events, regardless how deeply nested they are.
   */
  export class PuppeteerDevToolsConnection
    implements DevTools.CDPConnection.CDPConnection
  {
    readonly #connection: puppeteer.Connection;
    readonly #observers = new Set<DevTools.CDPConnection.CDPConnectionObserver>();
    readonly #sessionEventHandlers = new Map<
      string,
      puppeteer.Handler<unknown>
    >();
  
    constructor(session: puppeteer.CDPSession) {
      this.#connection = session.connection()!;
  
      session.on(
        CDPSessionEvent.SessionAttached,
        this.#startForwardingCdpEvents.bind(this),
      );
      session.on(
        CDPSessionEvent.SessionDetached,
        this.#stopForwardingCdpEvents.bind(this),
      );
  
      this.#startForwardingCdpEvents(session);
    }
  
    send<T extends DevTools.CDPConnection.Command>(
      method: T,
      params: DevTools.CDPConnection.CommandParams<T>,
      sessionId: string | undefined,
    ): Promise<
      | {result: DevTools.CDPConnection.CommandResult<T>}
      | {error: DevTools.CDPConnection.CDPError}
    > {
      if (sessionId === undefined) {
        throw new Error(
          'Attempting to send on the root session. This must not happen',
        );
      }
      const session = this.#connection.session(sessionId);
      if (!session) {
        throw new Error('Unknown session ' + sessionId);
      }
      // Rolled protocol version between puppeteer and DevTools doesn't necessarily match
      /* eslint-disable @typescript-eslint/no-explicit-any */
      return session
        .send(method as any, params)
        .then(result => ({result}))
        .catch(error => ({error})) as any;
      /* eslint-enable @typescript-eslint/no-explicit-any */
    }
  
    observe(observer: DevTools.CDPConnection.CDPConnectionObserver): void {
      this.#observers.add(observer);
    }
  
    unobserve(observer: DevTools.CDPConnection.CDPConnectionObserver): void {
      this.#observers.delete(observer);
    }
  
    #startForwardingCdpEvents(session: puppeteer.CDPSession): void {
      const handler = this.#handleEvent.bind(
        this,
        session.id(),
      ) as puppeteer.Handler<unknown>;
      this.#sessionEventHandlers.set(session.id(), handler);
      session.on('*', handler);
    }
  
    #stopForwardingCdpEvents(session: puppeteer.CDPSession): void {
      const handler = this.#sessionEventHandlers.get(session.id());
      if (handler) {
        session.off('*', handler);
      }
    }
  
    #handleEvent(
      sessionId: string,
      type: string | symbol | number,
      event: any, // eslint-disable-line @typescript-eslint/no-explicit-any
    ): void {
      if (
        typeof type === 'string' &&
        type !== CDPSessionEvent.SessionAttached &&
        type !== CDPSessionEvent.SessionDetached
      ) {
        this.#observers.forEach(observer =>
          observer.onEvent({
            method: type as DevTools.CDPConnection.Event,
            sessionId,
            params: event,
          }),
        );
      }
    }
  }
  