Skip to content

Include the CDP targetId in list_pages output #2366

Description

@thomasbachem

Is your feature request related to a problem? Please describe.

I run a thin stdio proxy in front of chrome-devtools-mcp that keeps page ids stable across restarts of the browser it drives (a dev app that gets re-exec'd at the same CDP port). The server reconnects fine on 1.6.0 and #2345 keeps ids unique, but a tab still gets a new id after a restart, so the proxy rewrites list_pages to hold one id per tab.

To do that it has to map each list_pages entry back to its CDP target, and there's no shared id to join on – list_pages gives title and url but not the CDP targetId. So the proxy fetches GET /json on the side and matches each entry by title, then url (consume-once). It's the most fragile thing the proxy does. Tabs that share a url are separable only by title – several of my tabs render the identical data: loader – so when titles also collide the match comes down to positional luck. And it's a second, independent read of browser state, so a tab opening or navigating between the /json fetch and the server's own view throws the listing off.

Describe the solution you'd like

Put each page's CDP targetId in list_pages output – ideally a field on the structuredContent.pages[] entries (and/or the text line). Then the mapping is just entry.targetId, with no /json round-trip to match up.

It's already on hand server-side (PageCollector reads it per page today), so it's a one-field addition to createStructuredPage() that leaves the existing fields alone – same shape as #2156, which added title. Happy to send the PR.

Describe alternatives you've considered

  • Keeping the GET /json + title/url heuristic – works today, but is the fragile part above
  • Reading the id client-side some other way – nothing in the MCP output keys on the CDP target, so it still means re-reading /json (or opening a separate CDP session) and re-matching, with the same fragility and a second source of truth to keep in sync

Additional context

targetId is stable per tab within a session and renews when the browser process restarts, same lifecycle as the underlying CDP id – I'm only after exposing it, not changing that.

Related: #2156 (added title), #2345 (unique ids across reconnects), #2304 (stale-listing selection retarget).

Activity

  1. changed the title [-]Include the CDP targetId in list_pages output[/-] [+]Include the CDP `targetId` in `list_pages` output[/+] on Jul 14, 2026
  2. added theissue type on Jul 15, 2026
  3. OrKoN commented on Jul 15, 2026

    @OrKoN
    Collaborator

    Thanks for the feature request but we do not plan to expose targetId via list_pages as for most users it has no utility. Could you use the experimental get_tab_id instead https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/tools/pages.ts#L392?

  4. thomasbachem commented on Jul 15, 2026

    @thomasbachem
    ContributorAuthor

    Thanks, that makes sense – for the common case I agree it's not worth putting on list_pages.

    I did give get_tab_id a try (with --experimental-interop-tools), but over --browserUrl it comes back empty for every page – no tabId in the response. Reproduced against both plain headless Chrome 150 and our Electron app (all tabs empty, selected or not). Happy to share the repro if it's useful.

    So for a --browserUrl setup it doesn't quite cover the mapping – I'll keep matching list_pages against GET /json on our side, which works fine, and close the PR. Thanks for the quick look!

  5. OrKoN commented on Jul 15, 2026

    @OrKoN
    Collaborator

    It's not in active use and uses internal Puppeteer APIs so it might have regressed (I am able to repro with and without browser URL).

  6. OrKoN commented on Jul 15, 2026

    @OrKoN
    Collaborator

    Interesting: I am not able to reproduce it in our test, there is an non-empty target ID.

  7. thomasbachem commented on Jul 15, 2026

    @thomasbachem
    ContributorAuthor

    FWIW, a page I drive with puppeteer directly always has a populated _tabId – whether I launch or connect, bundled build included. It only comes back empty through the running server's get_tab_id, in both launch and --browserUrl modes (on main too). So it doesn't seem to track the transport or launch/connect – looks specific to the page objects the server ends up with. Couldn't pin the exact spot, but happy to share the repro if it helps.

  8. thomasbachem commented on Jul 16, 2026

    @thomasbachem
    ContributorAuthor

    Pinned it down – it's not the page objects, and _tabId itself is fine. The value just never reaches the client.

    get_tab_id only writes it into structuredContent (McpResponse: structuredContent.tabId = this.#tabId) and never into the text response, and ToolHandler drops structuredContent entirely unless --experimentalStructuredContent is set. So the tool gates on --experimentalInteropTools but its only way out gates on a different flag – with interop alone it's always empty, and the text response is "" either way (checked on 1.6.0).

    That matches your repro through the server, and would explain why the test is the odd one out: reading structuredContent directly, the id is there and non-empty, while anything going through the server on interop alone never gets it emitted. It'd also cover why it hasn't come up otherwise – chrome-devtools.js has --experimentalStructuredContent in defaultArgs, so the flag is always on via chrome-devtools start.

    Happy to PR a fix – either pushing the id into the text response like the other fields, or an implies: 'experimentalStructuredContent' on experimentalInteropTools so it fails loudly instead. Or I can move it to its own issue if you'd rather track it separately.

    FWIW, one thing that came up on the way: _tabId is the tab target, while GET /json only lists type: page – for the same tab I get a tab target id out of get_tab_id and a different page target id out of /json, neither showing up in the other's list. So even fixed it wouldn't be a join key against /json – no bearing on this issue, just noting it. Staying on the /json matching either way.

  9. OrKoN commented on Jul 16, 2026

    @OrKoN
    Collaborator

    You can pass ?for_tab=true in the /json query to get tab targets. Now I recall that I did not add it to the test response to avoid parsing out of the text data but I think it makes sense to include in the text output.

  10. thomasbachem commented on Jul 16, 2026

    @thomasbachem
    ContributorAuthor

    Thanks – for_tab works, one catch: it has to be value-less. /json?for_tab returns the type: tab targets, and their ids match what get_tab_id returns (verified same-session), while ?for_tab=true is silently ignored (Chrome 150 here). Didn't know about that param – so the tab id is joinable after all. Our matching is cross-restart, where ids renew anyway, so we'll stay on title/url.

    Text output PR: #2381.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions