npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mcp-element-picker

v0.10.6

Published

Pick an element on any page in Chrome and hand it to your coding agent, over MCP.

Readme

MCP Element Picker

Point at an element on any page in Chrome, and the coding agent in your terminal gets the selector, the source file:line, computed styles, outerHTML and a cropped screenshot of that element.

Any MCP client can drive it. One global install and one folder loaded into Chrome; no build step, no bundler, no dependencies — plain Node ≥ 18 and an MV3 extension.

How it works

   Chrome                         localhost:5747                   agent
┌────────────┐              ┌───────────────────────┐          ┌───────────────────┐
│ content.js │  overlay     │                       │   MCP    │ pick_element      │
│ probe.js   │──selection──▶│    bridge/server.js   │◀─stdio──▶│ get_last_selection│
│ background │◀─long-poll───│   (the only place     │          │                   │
└────────────┘              │    state lives)       │          └───────────────────┘
                            └───────────────────────┘

Five parts, each doing exactly one thing:

| File | Role | |---|---| | extension/src/content.js | the overlay: highlight, event capture, building the element payload | | extension/src/probe.js | main-world probe: React/Vue/Svelte/Angular internals and file:line | | extension/src/background.js | the only piece allowed to reach 127.0.0.1 | | bridge/server.js | hub on port 5747, holds the state, two long-poll channels | | mcp/server.js | MCP server over stdio, JSON-RPC by hand, no SDK |

Why background.js and not a request straight from the page: a content script on an https page cannot reach http://127.0.0.1 — that is mixed content. The extension origin can.

Why probe.js is separate: framework expandos (__reactFiber$, __vueParentComponent) live in the main world, and a content script sits in an isolated world where they are invisible. The two talk through a marker attribute and postMessage.

Install

Four steps, once. Nothing to clone, nothing to download by hand.

1. Install the package. It carries both the server and the extension.

npm i -g mcp-element-picker

2. Register the server with your agent.

mcp-element-picker --install

3. Print where the extension lives. Copy the path it prints.

mcp-element-picker --extension-path

4. Load that folder into Chrome. chrome://extensions → turn on Developer modeLoad unpacked → paste the path from step 3.

Restart your agent, and /pick (Claude Code) or $pick (Codex) works in every project.

Install globally rather than through npx: the extension folder has to keep the same path, because Chrome remembers where an unpacked extension lives. A global install puts it at a stable place and npm update -g mcp-element-picker refreshes it in situ, so Chrome never notices. npx caches packages in a temporary directory that gets pruned, and Chrome would one day report the extension as missing.

Step 2 writes three files, all under your home directory:

| File | What goes in | |---|---| | ~/.claude.json | mcpServers.picker, merged in — other servers and keys are left alone | | ~/.claude/commands/pick.md | the /pick command | | ~/.claude/commands/picked.md | the /picked command |

With --client codex it is ~/.codex/config.toml (one [mcp_servers.picker] section) and ~/.agents/skills/{pick,picked}/SKILL.md instead. With no --client at all, every supported client found on the machine gets its own. Running it twice changes nothing.

Claude Code holds ~/.claude.json in memory while it runs and writes its own copy back on exit, so run step 2 with it closed. The same applies to claude mcp add.

Any other MCP client. The server is a plain stdio MCP server; nothing about it is specific to the two clients above. Put this wherever that client keeps its MCP servers:

{ "mcpServers": { "picker": { "command": "npx", "args": ["-y", "mcp-element-picker"] } } }

npx is used even after a global install: it finds the installed copy without going to the network, and it resolves the Windows .cmd shim that a bare command name would not.

| Client | Config file | |---|---| | Claude Code, all projects | ~/.claude.json, top-level mcpServers | | Claude Code, one project | .mcp.json in the project root | | Codex | ~/.codex/config.toml, [mcp_servers.picker] (TOML, not JSON) | | Claude Desktop | %APPDATA%\Claude\claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/) |

The setup panel — right-click the toolbar icon — shows the right command and snippet per client, with copy buttons, and opens that section by itself when nothing is connected.

From a clone. npx is wrong there — the config has to point at your files:

node /path/to/pickerPlugin/install.js                       # every client found here
node /path/to/pickerPlugin/install.js --client claude-code  # just one
node /path/to/pickerPlugin/install.js --project C:/app      # into one project's .mcp.json

Not on the Chrome Web Store. The extension ships inside the npm package instead, which is why step 3 exists. That also means it updates with the package rather than on Google's schedule — at the cost of Developer mode and Chrome's warning about it at startup.

How the pieces find each other

The bridge on port 5747 starts itself the first time an agent uses the picker, and needs no attention. If no agent is running yet the picker still works: the extension holds the selection and delivers it as soon as one connects.

The extension and the server never refer to each other by path — they meet on that port. So the extension can sit anywhere Chrome likes, the server can be updated underneath it, and neither breaks the other.

That also means several agents share one bridge. Claude Code and Codex running at once each spawn their own MCP server process, and whichever gets there first brings the bridge up; the other finds it and joins. They share one selection: if one agent reads it, it is marked read for both. Picks started by pick_element are routed back by request id, so two agents waiting at once each get their own click.

The bridge's lifetime is tied to whichever MCP server happened to spawn it — on Windows a child cannot outlive its parent's job object, detached or not. Restarting one agent can therefore take the bridge down for the other until the next tool call revives it. Nothing is lost but an unread selection, which lives in the bridge's memory.

Using it

Shortcuts. /pick in Claude Code, $pick in Codex: start the picker and wait for a click. With an argument they also state the task:

/pick make this button wider          (Claude Code)
$pick make this button wider          (Codex)
/picked   $picked                     take what was already picked with Alt+Shift+P

With no argument the agent just describes what was picked and changes nothing.

Both are plain markdown, installed globally so they work in every project:

| Client | Shape | Location | |---|---|---| | Claude Code | slash command, one .md per command | ~/.claude/commands/ | | Codex | skill, a directory with SKILL.md | ~/.agents/skills/ |

Codex used to read ~/.codex/skills and still does, but ~/.agents/skills is the cross-agent location it moved to; the installer writes there and clears out a copy an older version of itself left behind, so the same skill is not listed twice. For Claude Code, --project <dir> puts the commands in the project's .claude/commands/ instead, where they commit with the repo.

The agent asks you to point. You write "fix this button" — the agent calls pick_element, the overlay turns on in the active tab, you click, the agent gets the element. The tool blocks until you click, so the conversation does not break in half.

You pick first. Click the toolbar icon — or Alt+Shift+P — then click an element and type "make it wider". The agent takes it with get_last_selection, or you use /picked.

By default the icon picks straight away, because that is what you reach for it to do, and the setup panel sits on the icon's right-click menu, and on chrome://extensionsDetailsExtension options, which Chrome provides itself and which therefore works even if the menu does not. The panel has a checkbox that swaps the two round, if you would rather click for the panel and right-click to pick. Either way both actions stay on that menu, so nothing becomes unreachable, and Alt+Shift+P always picks.

Shift+click cannot be the second action, which is why it is a setting rather than a modifier: chrome.action.onClicked is handed only the tab and never the modifier keys, and it does not fire at all while the action has a popup. The setting works by putting that popup back — the same switch Chrome itself uses to decide which of the two happens.

In the overlay:

| Key | Action | |---|---| | click | pick and send | | Shift+click | add to the selection without sending, or take it back out | | Alt+click | pick just that one and jump to the note field | | Ctrl+ / Ctrl+ | parent / first child | | Ctrl+ / Ctrl+ | previous / next sibling, wrapping around | | Shift+Enter | send the selection with the note | | Esc | cancel — same as the × in the panel's corner |

Picking happens on pointerup, not on click: Chrome does not dispatch click at all on disabled form controls, so a disabled button could not be picked otherwise.

The panel can be dragged anywhere by its background — the cursor says so — and stays where you put it, on this and every later page. Its position is clamped back inside the viewport whenever it is shown or the window is resized, so a panel dragged to the edge of a wide screen is still reachable on a narrow one rather than stranded off it.

Hovering the panel itself highlights nothing — the picker ignores it. Next to the count of picked elements there is a clear button that empties the set without closing the picker.

There is always a note field at the bottom of the panel. Its text travels with the selection and reaches the agent as the first line — as an instruction, not as trailing detail. You can write it before clicking too: then a plain click sends the element along with the text.

The extension icon

The badge shows how many elements you picked that no agent has read yet. A selection made in response to pick_element never counts: it was taken immediately. Only what you picked on your own, by clicking the icon or pressing Alt+Shift+P, is counted.

A red ! instead of a number means there is no bridge — no agent is running. You can still pick; the extension holds the selection until a session appears.

The setup panel shows the same number, and a Clear button if you change your mind.

MCP tools

| Tool | What it does | |---|---| | pick_element | turns the picker on and waits for a click (prompt, screenshot, timeoutSeconds) | | get_last_selection | the last selection, without starting the picker | | picker_status | whether the bridge is alive and the extension connected | | cancel_pick | clear a wait that is stuck |

What the agent receives

The selector is built bottom-up and stops as soon as it is unique: #id first, then [data-testid], [name], then stable classes, and only if nothing else works, :nth-of-type(). Hashed classes (css-1a2b3c4, sc-xxxx, svelte-xxxx) are dropped from the selector but kept in the classes field. Computed styles are stripped of default values.

{
  "selector": "div[data-testid=\"checkout-card\"] > button.primary",
  "source": { "framework": "react", "file": "src/BuyButton.tsx", "line": 42, "column": 8 },
  "componentStack": ["BuyButton", "Card", "App"],
  "text": "Buy now",
  "rect": { "x": 43, "y": 434, "width": 87, "height": 37 },
  "styles": { "background-color": "rgb(124, 92, 255)", "border-radius": "6px" },
  "outerHTML": "<button class=\"primary\" type=\"submit\">Buy now</button>"
}

The element screenshot comes back as a separate MCP image block, so the agent actually sees it rather than reading a description.

Passwords, tokens and card fields are redacted before anything leaves the page: picking a login form must not carry credentials into the agent's context.

Framework support

Verified by running test/frameworks.html, which mounts each framework and puts a real exchange with probe.js through it:

| Stack | framework | source | componentStack | |---|---|---|---| | React 16–18 (classic JSX) | react | src/BuyButton.jsx:42:8 | yes | | React 19 / Next 15 | react | coordinates inside a bundle chunk | yes | | Vue 3 | vue | file without a line (__file) | yes | | Vue 2 | vue2 | file without a line (__file) | yes | | Svelte | svelte | src/Card.svelte:12:3 | no | | Angular | angular | no | yes | | Plain HTML | none | no | no |

About the method: React, Vue 3 and Vue 2 are the real thing, from a CDN. For React, __source is supplied by hand — exactly what the dev JSX transform does. Svelte and Angular are checked against the shape of their dev APIs (__svelte_meta, window.ng.getComponent), not against a real build, which would need their compilers. So what is verified there is our parsing, not their output.

React 19 removed _debugSource, so the source is dug out of fiber._debugStack instead. With webpack that frame carries a real path; with Turbopack it carries coordinates inside a chunk whose name is often a hash. Where the path looks like a chunk, the service worker fetches the source map and translates it back — see extension/src/sourcemap.js. Where the bundler already hands out real paths (webpack's eval devtools, Vite's raw ESM, React 16–18's compile-time _debugSource) no map is fetched at all.

On Turbopack that translation is unreliable, and measured to be so. The stack is walked fiber by fiber and stops at the first one carrying frames; sometimes those frames are the app's and the result is a real .tsx path, sometimes they are all Turbopack's own dev runtime and nothing usable comes back. Collecting frames across several fibers would fix it and is not done. On that stack, treat componentStack as the reliable handle: a component name greps in a second, which is most of what a file path would have bought.

Not detected at all: Preact, Solid, Qwik, Lit and other web components, Alpine, Ember. For those framework is "none" — the selector, styles, HTML and screenshot arrive as usual, only the component information is missing.

To run the matrix again:

npx --yes http-server -p 5801 .
# open http://127.0.0.1:5801/test/frameworks.html

Tests

npm test

Unit tests for the TOML section editor and the source-map reader — VLQ, mappings, path normalisation — and then an end-to-end run that starts the bridge, the MCP server and a fake extension and drives the whole path over real HTTP and JSON-RPC: selection, screenshot, the note, Esc, the unread counter, reading a selection back.

The overlay itself is checked by hand on a fixture that loads content.js and probe.js on an ordinary page with a chrome.runtime stub:

npx --yes http-server -p 5801 .
# open http://127.0.0.1:5801/test/fixture.html and press "Arm picker"

Known limits

  • One element, one top-level frame. Inside an <iframe> the iframe itself is picked (all_frames is off on purpose: otherwise the overlay is drawn in every frame).
  • Elements with pointer-events: none cannot be picked: the browser does not treat them as hit targets, and events go to an ancestor instead.
  • chrome://, the Web Store and extension pages are off limits to every extension, so the picker does not work there.
  • The screenshot is taken from the visible area, so the element has to be in the viewport at the moment you click.
  • The bridge listens on 127.0.0.1 only and exposes nothing outward, but it has no authentication: any local process can read the last selection.