scanner-typescript — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited scanner-typescript (Rules) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
registry/{scanName}.json for targeted readsbox, flex, etc.)matchCount, confidence)contains() + template XPathsinteractive (default), testId, full to control noise vs coverageget_registry_keys and get_locator vs full get_registry.cursor/mcp.json with ${workspaceFolder} supportflowchart 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 --> searchinteractive, testId, or full)matchCount on the live page, pick recommended + fallbacksregistry/{scanName}.json with keys, metadata, and locator bundlesgit clone https://github.com/Eswarr11/locator-mcp.git
cd locator-mcp # folder name may differ on your machine
npm install.cursor/mcp.json is preconfigured — reload MCP: Cmd+Shift+J → MCPscan_page with a scanName and cdpEndpoint| 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 |
# MCP server (stdio transport — used by Cursor / Claude)
npm run mcp
# Production
npm run build && npm startRun npm install in the repo before connecting.
Why not `cwd` + relative paths? Global~/.cursor/mcp.jsonignorescwd, so relative paths likesrc/server.tsresolve from your home directory and fail withERR_MODULE_NOT_FOUND. Use the committed project config ornpm run mcp --prefix <abs-path>.
.cursor/mcp.json is committed. Open this repo as your workspace — no manual edits needed:
{
"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.
For use across workspaces, add to ~/.cursor/mcp.json:
"locator-mcp": {
"command": "npm",
"args": ["run", "mcp", "--prefix", "<path-to-repo>"]
}Example for this machine:
"locator-mcp": {
"command": "npm",
"args": ["run", "mcp", "--prefix", "/Users/eswar/Desktop/locator-collector"]
}Claude does not support ${workspaceFolder}. Edit:
~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"locator-mcp": {
"command": "npm",
"args": ["run", "mcp", "--prefix", "<path-to-repo>"]
}
}
}Restart Claude Desktop after saving.
Run npm run build first, then point MCP at the compiled output:
"locator-mcp": {
"command": "node",
"args": ["<path-to-repo>/dist/server.js"]
}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):
{
"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:
open -a "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir=/tmp/pw-chromeStep 2 — Navigate to your target page using Playwright MCP tools.
Step 3 — Scan with locator-mcp:
{
"cdpEndpoint": "http://localhost:9222",
"scanName": "goal-side-panel-locators",
"scanMode": "interactive"
}Optional: pass pageUrl (substring) to target a specific tab when multiple are open.
scan_pageScan 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):
{
"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_registriesList all available registry files in registry/.
{ "registries": ["goal-side-panel-locators", "goal-create-side-panel-v2"] }get_registry_keysReturn 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_locatorFetch 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_registryReturn the full registry JSON. High token cost — prefer get_registry_keys + get_locator.
| Parameter | Required | Description |
|---|---|---|
scanName | Yes | Registry filename without .json |
search_registryFilter 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.
| 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.
Each scan writes registry/{scanName}.json — a map of element keys to metadata:
{
"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.
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.
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# Install dependencies
npm install
# Run MCP server in terminal (stdio)
npm run dev
# Run tests
npm test
# Compile TypeScript
npm run buildKey conventions:
.js extension: import { x } from './foo.js'data-testid: //tag[@data-testid="..."]registry/{scanName}.jsonSee .cursor/rules/ for full AI coding standards.
npm testUses Node built-in test runner (node:test) with tsx for TypeScript. Tests cover:
Committed:
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 |
Cannot find module '/Users/you/src/server.ts'Global ~/.cursor/mcp.json ignored cwd. Fix: use project .cursor/mcp.json or npm run mcp --prefix <abs-path>.
Failed to connect to browser--remote-debugging-port=9222--cdp-endpointopen -a "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir=/tmp/pw-chromeConnected to browser but no open page was foundOpen at least one tab in the debug Chrome instance before calling scan_page.
registry '{scanName}.json' not foundRun scan_page first, or call list_registries to see available files.
scanMode: "interactive" to reduce noisewarnings in the scan_page responseconfidence: "high" and matchCount: 1Remove locator-mcp from either global ~/.cursor/mcp.json or project .cursor/mcp.json — keep only one.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.