Skip to main content

lite-locator-mcp

Fewer LLM tokens and sturdier generated tests for Playwright test automation. Small local models (CPU, inside the package) find the page elements for each test step, so the LLM no longer reads whole pages or re-reads a growing conversation.

What it does

  • Runs manual test cases and writes the Playwright tests. Give it test cases as your team writes them ("Steps | Expected Result" rows with preconditions, Gherkin, one-line prose, jargon such as PLP / PDP / ATC). One LLM call plans each case; the library finds and acts on the elements itself and asks the LLM a short question only when it is unsure. Preconditions become setup steps, a sentence with several actions becomes single steps, dialogs and menus the case doesn't mention are opened first, browser popups are checked, and "any item" means the first one listed.
  • About 1,000 LLM tokens per test case (≈ $0.004 at GPT-5.4 prices, ≈ $0.15 for a 34-case user story), where an LLM agent loop used about 75,000 on the same cases, because the agent re-sends the whole conversation and page on every step.
  • Generated tests that keep passing. No database IDs or positional CSS in the scripts: elements are located by label, card heading, table row or text, only when that matches exactly one element. Scripts wait for what the app changed (new page title, a badge going from 1 to 2, a popup) and pass with parallel workers. A test case that fails never produces a passing script: it is written as test.fixme with the failing step.
  • Works with small local models. Measured with a 4B model on a laptop CPU/GPU:
Test suite Passed Generated scripts passing npx playwright test
34 written test cases for one user story (practicesoftwaretesting.com: search, filters, sorting, product page, quantity, cart) 33–34 / 34 across runs all passing cases
16 messy test cases on another site (demoblaze.com: step/expected rows, prose, Gherkin, jargon; one has no test data on purpose) 13 / 16 (13 of the 15 that can pass) all passing cases but one
  • Finds elements for LLM agents too. For an existing LangGraph / Playwright MCP agent: a short list of candidate elements (40–150 tokens) instead of the whole page (2,000–12,000), what changed after an action instead of a new snapshot, and stable locators in the code Playwright MCP records. Icon-only buttons (✕, 🗑, +) are found by icon and table row.

Runs on CPU, no GPU and no PyTorch; the int8 ONNX models are inside the package and nothing is downloaded at runtime.

Install

You use Install Notes
TestRunner or your own Python code with a Playwright page pip install lite-locator-mcp playwright then python -m playwright install chromium
LangChain / LangGraph with langchain-mcp-adapters pip install lite-locator-mcp Works with any mcp 1.x (tested with 1.3). Don't add [mcp]: langchain-mcp-adapters needs mcp 1.x.
An MCP client (Claude Code, Cursor, Copilot) pip install "lite-locator-mcp[mcp]" The proxy needs mcp ≥ 2.3; put it in its own venv if something else needs mcp 1.x.

Python 3.10+. Node 18+ for Playwright MCP and for running the generated .spec.js files. On Windows, onnxruntime needs the Microsoft Visual C++ 2015–2022 runtime (usually already installed).

1. Run manual test cases and get Playwright scripts (TestRunner)

from langchain_ollama import ChatOllama          # or AzureChatOpenAI, or any function: prompt -> text
from playwright.sync_api import sync_playwright
from lite_locator.runner import TestRunner

case = """Title: Search shows matching products
1. Open https://practicesoftwaretesting.com
2. Type pliers in the search box
3. Click the search button
4. Verify the text "Searched for: pliers" is shown
5. Verify the product Thor Hammer is not listed"""

llm = ChatOllama(model="qwen3:4b-instruct", temperature=0)
runner = TestRunner(llm, planner=ChatOllama(model="qwen3:4b-instruct", temperature=0, format="json"))
with sync_playwright() as p:
    result = runner.run(p.chromium.launch().new_page(), case)

print(result.passed)          # True
for s in result.log:          # one entry per step: what was done, and who chose the element
    print(s["ok"], s["chosen_by"], s["step"])
print(result.usage)           # LLM calls and tokens: {'plan': {'calls': 1, ...}, 'choose': {...}}
open("search.spec.js", "w").write(result.spec("Search shows matching products"))

A whole suite from the command line (examples/run_test_cases.py): runs every case, re-runs failures with the LLM choosing every element (the healer), writes one .spec.js per case, runs them with Playwright Test and prints the token usage.

python examples/run_test_cases.py --cases cases.txt --out specs/ --check-scripts
# TC01 PASS  Search by name shows matching products
# ...
# LLM usage
#   run     53 calls    18,410 input    4,732 output tokens
#   heal    15 calls     4,003 input      351 output tokens
#   34/34 test cases passed; scripts in specs/

A generated script (cart, two products, delete one; note the waits and the row-scoped icon button):

await page.goto("https://practicesoftwaretesting.com");
await page.waitForLoadState('networkidle');
await page.getByRole("heading", { name: "Combination Pliers", exact: true }).click();
await expect(page).toHaveTitle("Combination Pliers - Practice Software Testing - Toolshop - v5.0");
await page.locator('[data-test="add-to-cart"]').click();
await expect(page.locator("[data-test=\"nav-cart\"]")).toHaveText("1");
// ... second product, open the cart ...
await page.getByRole('row').filter({ hasText: "Claw Hammer" }).locator("a.btn.btn-danger").click();
await expect(page.getByText("Claw Hammer").filter({ visible: true })).toHaveCount(0);

The test case can be in any format your team uses (step / expected rows, Gherkin, prose). What it can't do: invent data the case doesn't give ("log in with valid credentials"), so put the data in the test case.

Quality guards: the library acts alone only when its top match is clearly ahead, fits the action (type → a text box) and is unique, or when the element's own title is exactly what the step names ("Claw Hammer", not "Claw Hammer with Shock Reduction Grip"); a click that changes nothing goes back to the LLM; verify steps are checked in the browser by code, and a value mismatch is shown to the LLM before the step fails; values come from the test case text, not from the model's copy of it. Step types: open, click, type, select, check, uncheck, press, back, verify text / absent / URL / value.

2. LangGraph agent with Playwright MCP (three lines change)

from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
from lite_locator.langchain import with_lite_locator, LOCATOR_HINT, snapshot_hook

tools = with_lite_locator(await load_mcp_tools(session), lean=True)   # ★1 find tool, change reports, stable code
agent = create_react_agent(llm, tools,
                           prompt=LOCATOR_HINT + your_prompt,          # ★2 find first; full snapshot only when needed
                           pre_model_hook=snapshot_hook)               # ★3 old full snapshots are not re-read

What the LLM then sees (real output, practicesoftwaretesting.com, Playwright MCP 0.0.83):

browser_find_element(step="Check the Hammer category filter")
  - checkbox "Hammer" [ref=e213]  (section: By category:)
  - img "Hammer" [ref=e384]  (section: Hammer)
  - heading "Hammer" [ref=e390]  (section: Combination Pliers)
  (3 of 126 elements, ~40 tokens instead of ~3324 for the full page.)

browser_click(target="e213")  ->
  ### Ran Playwright code
  await page.getByLabel("Hammer", { exact: true }).click();      <- Playwright MCP recorded [data-test="category-01M672…"]
  ### Result
  Same page. Changes:
  ~ checkbox "Hammer"  ->  checkbox "Hammer" [checked] [ref=e213]
  - link "Combination Pliers … $14.15"
  ...

Options: diffs=False keeps the action tools unchanged; keep={"browser_navigate", "browser_click", ...} shows the LLM only those tools (tool definitions are re-sent on every call) while the library still uses the others it needs; stable_code=False keeps Playwright MCP's recorded locators as they are; lean=True drops tool options a test agent doesn't need (doubleClick, modifiers, slowly): fewer tokens per call, and typing replaces a field's value instead of appending to it. Your Playwright MCP server, its version and flags stay as they are. Full examples: examples/quickstart_langgraph.py, examples/agent_testgen.py.

3. Your own Python code with a Playwright page

from lite_locator import Locator, act

loc = Locator()                                          # loads the models once (~2 s)
hits = loc.find(page, "Click the product Combination Pliers")
hits.prompt        # the shortlist as text for your LLM prompt (it picks one)
hits[0]["py"]      # 'page.locator("a").filter(has=page.get_by_role("heading", name="Combination Pliers", exact=True))'
hits.tokens, hits.full_tokens                            # ~165 vs ~3382

print(act(page, hits[0], "click"))
# Navigated to a new page. Page: Combination Pliers - Practice Software Testing - Toolshop - v5.0  .../product/...
print(act(page, loc.find(page, "Click the Add to cart button")[0], "click"))
# Same page. Changes: + text: 1  + alert "Product added to shopping cart."

act(page, hit, action, value) actions: click, dblclick, hover, fill, type, select, check, uncheck, press. Async pages: await loc.afind(...), await aact(...). to_locator(page, hit) gives a live Playwright Locator. Full example: examples/quickstart_page.py.

4. MCP client (proxy in front of Playwright MCP)

{"mcpServers": {"playwright": {"command": "lite-locator-mcp", "args": ["--", "--headless", "--isolated"]}}}

All Playwright MCP tools stay available; browser_find_element(step) is added and actions return what changed. Arguments after -- go to Playwright MCP. Options: --lam 0.005|0.01|0.02 (more / default / fewer candidates), --full-snapshots (actions return Playwright's own full snapshot), --lean (start Playwright MCP with --snapshot-mode none). PLAYWRIGHT_MCP overrides the child command (default npx -y @playwright/mcp@0.0.83).

How "what changed" behaves

After the action The LLM gets
A new page Navigated to a new page. + title, URL, main headings, what can be done there
Part of the page changed Same page. Changes: + + added, - removed, ~ changed elements, with the card (product) for repeated buttons
Nothing changed, even after waiting up to 3 s for slow responses No change detected within 3 s. If this action submits, pays or deletes something, do not repeat it: check the page with browser_snapshot first.
A new tab or a browser dialog A new tab opened: … / A browser dialog appeared: '…' (it was dismissed); act(..., dialog="accept") to accept

Changes that happen by themselves (tickers, carousels, timers) are filtered out: the page is compared with itself just before the action.

Limits

  • Checks over a whole page or list (counts, "all items", sorted order): TestRunner checks texts, URLs and single values; an agent should use browser_snapshot for these (the hint says so).
  • The right element is outside the shortlist for roughly 10–18% of hard steps; TestRunner then fails the step (the healer retries with the LLM choosing), an agent takes the full snapshot.
  • English pages only (the models are English).
  • Visual-only changes (colours, canvas charts) are invisible in accessibility snapshots.
  • Test cases still need a human review: a wrong expectation in a test case is reported as a failure, as it should be.

Development and release checks

Every change is recorded in CHANGELOG.md (what, why, how it was checked). ci/check.sh runs lint (ruff), a security scan (bandit), a secrets scan, a dependency vulnerability audit (pip-audit), the package build and metadata checks, the test suite on local test pages in three environments (proxy with mcp 2.x, LangChain with the latest adapters, and next to mcp 1.3), and a ranking quality gate against a recorded baseline. The report goes to ci/reports/<version>.md; ci/publish.sh uploads only an all-green report of the current commit.

The training code (data collection, model training and evaluation) is not part of this repository; the package ships the trained models.

Credits and licences

Metadata

Release files for lite-locator-mcp 0.1.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lite-locator-mcp 0.1.8
File Size Uploaded Provenance Actions
lite_locator_mcp-0.1.8.tar.gz 38.2 MB Without provenance Details for lite_locator_mcp-0.1.8.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for lite-locator-mcp 0.1.8
File Interpreter ABI Platform Actions
lite_locator_mcp-0.1.8-py3-none-any.whl Python 3 none any Details for lite_locator_mcp-0.1.8-py3-none-any.whl

Total release size: 76.5 MB

Release files / lite_locator_mcp-0.1.8.tar.gz

Download URL lite_locator_mcp-0.1.8.tar.gz
Size 38.2 MB
Tags Source
SHA-256 checksum
How to use checksums
9da36fc5370b87f0a32ec30285a143e350bc97c382408b0f8d330b100e2fa1cd
BLAKE2b-256 checksum
How to use checksums
d73f2244ae03e1ba115bbd492749cede4174a53e43cfce1ff319d360cbb55e97
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / lite_locator_mcp-0.1.8-py3-none-any.whl

Download URL lite_locator_mcp-0.1.8-py3-none-any.whl
Size 38.3 MB
Tags Python 3
SHA-256 checksum
How to use checksums
733a157571d01c8f7de3a403c037c5187780541bde8a8979b83ab9f17a97a58a
BLAKE2b-256 checksum
How to use checksums
d53192804dd8ff602a7c09ecb7c429876fece0d48d365672f0c7cfd388e696e6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Security for lite-locator-mcp 0.1.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page