Skip to main content
Glama
Eswarr11
by Eswarr11
README.md
# locator-mcp

MCP server that scans live browser pages and builds structured locator registries for test automation. Connects to Chrome via CDP or Playwright WebSocket, discovers elements, generates ranked XPath/CSS/testId locators (including relational XPaths for duplicate `data-testid` values), validates uniqueness on the page, and saves per-page JSON registries that AI clients can read incrementally.

Built for use alongside **Playwright MCP** — navigate and interact with the browser using Playwright MCP, then call `scan_page` to capture locators into a registry.

---

## Table of contents

- [Features](#features)
- [How it works](#how-it-works)
- [Prerequisites](#prerequisites)
- [Quick start](#quick-start)
- [Run locally](#run-locally)
- [MCP configuration](#mcp-configuration)
- [MCP tools reference](#mcp-tools-reference)
- [Scan modes](#scan-modes)
- [Registry format](#registry-format)
- [Token-efficient workflow](#token-efficient-workflow)
- [Project structure](#project-structure)
- [Development](#development)
- [Testing](#testing)
- [Git conventions](#git-conventions)
- [Troubleshooting](#troubleshooting)

---

## Features

- **Live page scanning** — connects to an already-open browser tab (no headless relaunch)
- **Per-page registries** — each scan saves to `registry/{scanName}.json` for targeted reads
- **Relational XPath** — ancestor-scoped and label-sibling XPaths disambiguate duplicate generic testIds (`box`, `flex`, etc.)
- **Multi-strategy locators** — testId, id, aria, form attributes, text, class, positional fallbacks
- **Uniqueness validation** — each locator is checked on the live page (`matchCount`, `confidence`)
- **Dynamic testId templates** — detects unstable suffixes and emits `contains()` + template XPaths
- **Three scan modes** — `interactive` (default), `testId`, `full` to control noise vs coverage
- **Token-efficient read tools** — `get_registry_keys` and `get_locator` vs full `get_registry`
- **Portable MCP config** — committed `.cursor/mcp.json` with `${workspaceFolder}` support

---

## How it works

```mermaid
flowchart LR
  subgraph browser [Browser]
    page[Open tab via CDP/WS]
  end
  subgraph scan [scan_page pipeline]
    discover[discover-elements]
    extract[extract-element-context]
    generate[generate-xpath-variants]
    rank[rank-xpath-variants]
    validate[validate-locator]
    save[save-registry]
  end
  subgraph registry [registry/]
    json["{scanName}.json"]
  end
  subgraph read [Read tools]
    keys[get_registry_keys]
    one[get_locator]
    search[search_registry]
  end
  page --> discover --> extract --> generate --> rank --> validate --> save --> json
  json --> keys
  json --> one
  json --> search
```

1. **Discover** — query the DOM using mode-specific selectors (`interactive`, `testId`, or `full`)
2. **Extract context** — per element: attributes, direct text, ancestor chain (up to 6 levels), label siblings
3. **Generate variants** — XPath candidates across 9 tiers (testId → relational → text → class → positional)
4. **Rank & validate** — score variants, verify `matchCount` on the live page, pick recommended + fallbacks
5. **Save** — write `registry/{scanName}.json` with keys, metadata, and locator bundles
6. **Read** — AI client fetches keys first, then individual entries as needed

---

## Prerequisites

- **Node.js** 18+ (tested on Node 24)
- **npm**
- **Google Chrome** (for CDP mode) or a Playwright browser server (for WebSocket mode)
- **Cursor** or **Claude Desktop** for MCP integration

---

## Quick start

```bash
git clone https://github.com/Eswarr11/locator-mcp.git
cd locator-mcp          # folder name may differ on your machine
npm install
```

1. Open the cloned folder as your **Cursor workspace**
2. [`.cursor/mcp.json`](.cursor/mcp.json) is preconfigured — reload MCP: **Cmd+Shift+J → MCP**
3. Launch Chrome with remote debugging (see [With Playwright MCP](#with-playwright-mcp-for-scan_page))
4. Navigate to your target page using Playwright MCP
5. Call `scan_page` with a `scanName` and `cdpEndpoint`

---

## Run locally

| Command | Description |
|---------|-------------|
| `npm run mcp` | Start MCP server via `tsx` — used by Cursor / Claude |
| `npm run dev` | Same as `mcp`, for terminal development |
| `npm run build` | Compile TypeScript to `dist/` |
| `npm start` | Run compiled server (`node dist/server.js`) |
| `npm test` | Run unit tests |

```bash
# MCP server (stdio transport — used by Cursor / Claude)
npm run mcp

# Production
npm run build && npm start
```

---

## MCP configuration

Run `npm install` in the repo before connecting.

> **Why not `cwd` + relative paths?** Global `~/.cursor/mcp.json` ignores `cwd`, so relative paths like `src/server.ts` resolve from your home directory and fail with `ERR_MODULE_NOT_FOUND`. Use the committed **project** config or `npm run mcp --prefix <abs-path>`.

### Cursor (project) — recommended

[`.cursor/mcp.json`](.cursor/mcp.json) is committed. Open this repo as your workspace — no manual edits needed:

```json
{
  "mcpServers": {
    "locator-mcp": {
      "command": "npm",
      "args": ["run", "mcp", "--prefix", "${workspaceFolder}"]
    }
  }
}
```

`${workspaceFolder}` resolves to the project root automatically. Reload MCP after clone: **Cmd+Shift+J → MCP**.

If `locator-mcp` is also defined in global `~/.cursor/mcp.json`, remove one entry to avoid duplicate servers.

### Cursor (global)

For use across workspaces, add to `~/.cursor/mcp.json`:

```json
"locator-mcp": {
  "command": "npm",
  "args": ["run", "mcp", "--prefix", "<path-to-repo>"]
}
```

Example for this machine:

```json
"locator-mcp": {
  "command": "npm",
  "args": ["run", "mcp", "--prefix", "/Users/eswar/Desktop/locator-collector"]
}
```

### Claude Desktop

Claude does not support `${workspaceFolder}`. Edit:

`~/Library/Application Support/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "locator-mcp": {
      "command": "npm",
      "args": ["run", "mcp", "--prefix", "<path-to-repo>"]
    }
  }
}
```

Restart Claude Desktop after saving.

### Production (compiled)

Run `npm run build` first, then point MCP at the compiled output:

```json
"locator-mcp": {
  "command": "node",
  "args": ["<path-to-repo>/dist/server.js"]
}
```

### With Playwright MCP (for `scan_page`)

`scan_page` does not launch a browser — it connects to one that is already open. Pair with Playwright MCP on the same CDP endpoint (typically in global `~/.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": [
        "@playwright/mcp@latest",
        "--cdp-endpoint",
        "http://localhost:9222"
      ]
    },
    "locator-mcp": {
      "command": "npm",
      "args": ["run", "mcp", "--prefix", "<path-to-repo>"]
    }
  }
}
```

**Step 1 — Launch Chrome with remote debugging:**

```bash
open -a "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir=/tmp/pw-chrome
```

**Step 2 — Navigate** to your target page using Playwright MCP tools.

**Step 3 — Scan** with locator-mcp:

```json
{
  "cdpEndpoint": "http://localhost:9222",
  "scanName": "goal-side-panel-locators",
  "scanMode": "interactive"
}
```

Optional: pass `pageUrl` (substring) to target a specific tab when multiple are open.

---

## MCP tools reference

### `scan_page`

Scan an open browser page and save locators to `registry/{scanName}.json`.

| Parameter | Required | Description |
|-----------|----------|-------------|
| `cdpEndpoint` | One of CDP/WS | CDP HTTP URL, e.g. `http://localhost:9222` |
| `wsEndpoint` | One of CDP/WS | Playwright WebSocket URL, e.g. `ws://127.0.0.1:PORT/...` |
| `scanName` | Yes | Registry filename without `.json` |
| `scanMode` | No | `interactive` (default), `testId`, or `full` |
| `pageUrl` | No | URL substring to pick a specific tab |

**Example response (abbreviated):**

```json
{
  "scanId": "goal-side-panel-locators",
  "registryFile": "registry/goal-side-panel-locators.json",
  "pageUrl": "https://app.example.com/goals",
  "scannedAt": "2026-06-23T12:00:00.000Z",
  "totalElements": 42,
  "registrySaved": true,
  "stats": {
    "total": 42,
    "unique": 38,
    "duplicate": 4,
    "lowConfidence": 2,
    "interactive": 30
  },
  "warnings": [
    "5 elements share generic testId 'box' — relational XPath applied"
  ],
  "sample": [ "...first 5 elements..." ]
}
```

---

### `list_registries`

List all available registry files in `registry/`.

```json
{ "registries": ["goal-side-panel-locators", "goal-create-side-panel-v2"] }
```

---

### `get_registry_keys`

Return a lightweight list of element keys — **use this before fetching individual locators**.

| Parameter | Required | Description |
|-----------|----------|-------------|
| `scanName` | Yes | Registry filename without `.json` |

**Returns per key:** `key`, `tagName`, `testId`, `confidence`, `matchCount`, `strategy`

Much smaller than `get_registry` — typically 90%+ token savings on large pages.

---

### `get_locator`

Fetch a single element entry by key.

| Parameter | Required | Description |
|-----------|----------|-------------|
| `scanName` | Yes | Registry filename without `.json` |
| `key` | Yes | Element key from `get_registry_keys` |

**Returns:** `key`, `tagName`, `text`, `attributes`, `locators` (`recommended`, `fallbacks`, `xpath`, `confidence`, `matchCount`, `strategy`)

---

### `get_registry`

Return the **full** registry JSON. High token cost — prefer `get_registry_keys` + `get_locator`.

| Parameter | Required | Description |
|-----------|----------|-------------|
| `scanName` | Yes | Registry filename without `.json` |

---

### `search_registry`

Filter registry entries by attributes.

| Parameter | Required | Description |
|-----------|----------|-------------|
| `scanName` | Yes | Registry filename without `.json` |
| `tagName` | No | Exact HTML tag match, e.g. `button` |
| `testId` | No | Substring match on `data-testid` |
| `text` | No | Substring match on direct text |
| `confidence` | No | `high`, `medium`, or `low` |

All provided filters are combined with AND logic.

---

## Scan modes

| Mode | Selectors | Best for |
|------|-----------|----------|
| `interactive` (default) | Buttons, inputs, links, roles + attributed elements | Most UI pages — low noise, high signal |
| `testId` | `data-testid`, `data-test`, `data-qa`, `data-cy` only | Apps with consistent test IDs |
| `full` | All candidate attributes including `[class]` | Maximum coverage — expect more noise |

**Recommendation:** start with `interactive`. Switch to `testId` when the app has good testId coverage. Use `full` only when you need exhaustive discovery.

---

## Registry format

Each scan writes `registry/{scanName}.json` — a map of element keys to metadata:

```json
{
  "saveButton": {
    "key": "saveButton",
    "tagName": "button",
    "text": "Save",
    "attributes": {
      "testId": "goal-form_button_save",
      "id": null,
      "role": null,
      "ariaLabel": null,
      "placeholder": null
    },
    "locators": {
      "recommended": {
        "xpath": "//button[@data-testid='goal-form_button_save']",
        "tier": 1,
        "strategy": "testId",
        "matchCount": 1,
        "confidenceScore": 90
      },
      "fallbacks": [ "..." ],
      "xpath": "//button[@data-testid='goal-form_button_save']",
      "confidence": "high",
      "matchCount": 1,
      "strategy": "testId"
    }
  }
}
```

**Locator confidence:**

| Level | Meaning |
|-------|---------|
| `high` | `matchCount === 1`, stable strategy (testId, id, relational) |
| `medium` | Unique but weaker strategy (text, aria) |
| `low` | `matchCount > 1` or positional/generic fallback |

Registry JSON files are **gitignored** — generated locally via `scan_page`. Only `registry/.gitkeep` is committed to preserve the folder.

---

## Token-efficient workflow

For AI clients reading registries, follow this order to minimize token usage:

```
1. list_registries          → see what's available
2. get_registry_keys        → lightweight key list (~500–4k tokens)
3. get_locator (per key)    → single entry (~100–200 tokens each)
   OR search_registry       → filtered subset

Avoid: get_registry         → full file (10k–80k+ tokens)
```

| Registry size | `get_registry` (est.) | `get_registry_keys` (est.) |
|---------------|----------------------|---------------------------|
| ~39 KB | ~10,000 tokens | ~500–1,000 tokens |
| ~321 KB | ~80,000 tokens | ~2,000–4,000 tokens |

Estimates use `characters / 4` — good for relative comparison, not exact billing.

---

## Project structure

```
locator-mcp/
├── .cursor/
│   ├── mcp.json              # Cursor MCP config (committed, portable)
│   └── rules/                # Cursor AI rules for this repo
├── registry/
│   └── .gitkeep              # Scan output dir (JSON files gitignored)
├── src/
│   ├── server.ts             # MCP tool definitions
│   ├── index.ts              # Package entry
│   ├── scanner/
│   │   ├── scanner.service.ts        # Scan orchestration
│   │   ├── discover-elements.ts      # DOM element discovery by scan mode
│   │   ├── extract-element-context.ts # Attributes, ancestors, labels
│   │   ├── generate-xpath-variants.ts
│   │   ├── generate-relational-xpath.ts
│   │   ├── rank-xpath-variants.ts
│   │   ├── validate-locator.ts
│   │   ├── generate-key.ts
│   │   ├── detect-dynamic-value.ts
│   │   ├── generate-locators.ts
│   │   ├── save-registry.ts
│   │   └── scanner.types.ts
│   └── shared/
│       ├── constants.ts      # Selectors, scan modes, deny lists
│       ├── registry.ts       # Registry file I/O
│       └── utils.ts
├── tests/
│   └── scanner.spec.ts       # Unit tests
├── package.json
├── tsconfig.json
└── README.md
```

---

## Development

```bash
# Install dependencies
npm install

# Run MCP server in terminal (stdio)
npm run dev

# Run tests
npm test

# Compile TypeScript
npm run build
```

**Key conventions:**

- ESM imports with `.js` extension: `import { x } from './foo.js'`
- XPath locators prefer `data-testid`: `//tag[@data-testid="..."]`
- Relational XPaths disambiguate duplicate generic testIds
- Registry path: `registry/{scanName}.json`

See [`.cursor/rules/`](.cursor/rules/) for full AI coding standards.

---

## Testing

```bash
npm test
```

Uses Node built-in test runner (`node:test`) with `tsx` for TypeScript. Tests cover:

- XPath variant generation and scoring
- Generic testId disambiguation and relational XPath
- Dynamic value detection and templates
- Key generation with ancestor context
- Constants and denylist behavior

---

## Git conventions

**Committed:**

- Source (`src/`), tests, config, `.cursor/mcp.json`, `.cursor/rules/`
- `registry/.gitkeep` (empty folder placeholder)

**Not committed (`.gitignore`):**

| Path | Reason |
|------|--------|
| `node_modules/` | Dependencies — run `npm install` |
| `dist/` | Build output — run `npm run build` |
| `registry/*.json` | Local scan data — run `scan_page` |
| `.env*` | Secrets |

---

## Troubleshooting

### `Cannot find module '/Users/you/src/server.ts'`

Global `~/.cursor/mcp.json` ignored `cwd`. Fix: use project [`.cursor/mcp.json`](.cursor/mcp.json) or `npm run mcp --prefix <abs-path>`.

### `Failed to connect to browser`

- Chrome must be running with `--remote-debugging-port=9222`
- Playwright MCP must use the same `--cdp-endpoint`
- Relaunch Chrome:

```bash
open -a "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir=/tmp/pw-chrome
```

### `Connected to browser but no open page was found`

Open at least one tab in the debug Chrome instance before calling `scan_page`.

### `registry '{scanName}.json' not found`

Run `scan_page` first, or call `list_registries` to see available files.

### Duplicate / low-confidence locators

- Use `scanMode: "interactive"` to reduce noise
- Check `warnings` in the `scan_page` response
- Prefer entries with `confidence: "high"` and `matchCount: 1`
- For duplicate generic testIds, relational XPaths are generated automatically

### MCP server shows twice in Cursor

Remove `locator-mcp` from either global `~/.cursor/mcp.json` or project `.cursor/mcp.json` — keep only one.

TDQS

A4/5.0

Scored across 6 tools

Disambiguation5/5

Each tool targets a distinct operation: retrieving a single locator, full registry, keys list, registry listing, scanning, or searching. No overlap or ambiguity.

Naming Consistency5/5

All tools follow a consistent verb_noun pattern with snake_case: get_locator, get_registry, get_registry_keys, list_registries, scan_page, search_registry.

Tool Count5/5

Six tools is well-scoped for a locator management server, covering scanning, retrieval, listing, and searching without being excessive or insufficient.

Completeness5/5

The tool set covers the full lifecycle: scanning pages to extract locators, retrieving entire registries or specific entries, searching, and listing. No obvious gaps.

Maintenance

ActivityStale
ResponsivenessNo issues