optics-framework
OfficialDrives Android devices and emulators (including Android TV) to run automated test cases, locating and interacting with UI elements through the device's accessibility tree, OCR, or image matching.
Uses Appium as a mobile driver backend, enabling AI agents to execute keywords against Android and iOS apps and read back device/screen state through MCP resources.
Provides a BLE driver that controls production devices as a Bluetooth HID mouse/keyboard when debugging and screenshots are blocked, using coordinate-based interaction paired with screenshot and vision strategies.
Integrates Google Vision as an OCR engine for the text-detection locator strategy, finding UI elements on screenshots when the element tree cannot be used.
Automates iOS applications and devices via the framework's mobile driver backends, running the same keyword-based test steps used for Android.
Enables OpenAI-powered natural-language test authoring and AI self-heal recovery, letting an LLM read the screen and re-enter the locator ladder when all locator strategies fail.
Targets Samsung Tizen TVs, allowing test keywords to be executed against Samsung smart TV apps through the mobile driver backend.
Drives web browsers through Selenium, letting agents execute test keywords against web applications and inspect the resulting page state.
Optics Framework
Self-healing test automation for mobile, web, TV — and AI agents.
One keyword engine. Six ways to drive it: CSV/YAML files, a Python SDK, Robot Framework, a REST API, an interactive terminal, or an MCP server your AI agent talks to.

Documentation · Install · Getting Started · Keywords · Architecture
Most frameworks assume a UI element has one true locator, and a test breaks the moment that locator changes. Optics assumes an element has several plausible identities — its XPath, its visible text, what it looks like on screen — and tries all of them before giving up.
That idea runs through the whole framework: locators fall back, drivers fall back, element values fall back. Tests are data (CSV or YAML), so non-coders can write them, and the same keywords are reachable from Python, Robot Framework, HTTP, and MCP.
Install
One line puts the optics CLI on your PATH:
curl -fsSL https://optics-framework.org/install | sh # macOS, Linuxirm https://optics-framework.org/install.ps1 | iex # WindowsIt installs into its own virtual environment under ~/.optics, so nothing lands in your system Python. Then optics quickstart builds a project, checks your setup, and runs your first test.
Already managing your own environment? Optics needs Python 3.12+, and the core install ships no drivers, OCR, or LLM backends — you add only what you need:
python3 -m venv venv && source venv/bin/activate
pip install "optics-framework[appium,easyocr]"Most extra names match the config.yaml source keys, so the word you install is usually the word you enable (exceptions: google-vision enables google_vision, and the bundles and mcp aren't config keys):
Drivers |
|
OCR |
|
AI |
|
Bundles |
|
optics setup installs the drivers, OCR and LLM engines above (and the bundles) by name — pinned to your installed Optics version — and bare optics setup opens a TUI picker. optics quickstart goes one step further and offers the right extras for the platform you pick:
optics setup --list
optics setup --install appium easyocrThemcp server extra is pip-only (it isn't an engine backend optics setup manages): pip install "optics-framework[mcp]".
Some extras need system tooling beyond the Python package. A driver extra installs only thePython client — mobile testing also needs the Appium server, a device/emulator, and platform tooling (Node.js, Android SDK/adb, JDK). pytesseract needs the Tesseract binary; playwright needs its browsers (playwright install). See the Installation guide.
Conda is not supported foreasyocr + optics-framework together (conflicting NumPy 1.x/2.x requirements). Use a standard venv.
Related MCP server: adb-mcp
Quickstart
From a fresh install to a running test with one guided command:
pip install optics-framework
optics quickstartoptics quickstart asks what you want to automate, installs the matching engine, scaffolds the project, writes a platform-correct config.yaml, and runs an optics doctor check — then prints your next steps. Every prompt has a default, so pressing Enter throughout yields a runnable project.
Prefer to drive each step yourself?
optics init my_test_project --template contact
# point my_test_project/config.yaml at your device/app, start the Appium server, then:
optics dry_run my_test_project # validate keywords, elements and module refs — no device (but the config's engines must be installed)
optics execute my_test_project--template scaffolds a working project from a bundled sample. Each needs the matching extras installed (see Install); the headline [appium,easyocr] above covers the first three:
contact·calendar·youtube— Appium/Android, easyocr →[appium,easyocr]clock— Appium/Android, image templates + Tesseract OCR →[appium,pytesseract](plus the Tesseract binary)gmail_web— Selenium →[selenium,easyocr]playwright— Playwright →[playwright](thenplaywright installfor the browsers)
Omit --template for an empty scaffold with a commented starter config.yaml. Need only a config for an existing folder? optics configure <folder> writes one from a few questions, and optics doctor [folder] [--check] verifies engines, tooling, and project config.
Why Optics
A locator ladder, not a locator. Every element-based keyword walks a priority-ordered chain until one strategy succeeds:
# | Strategy | How it finds the element |
1 |
| Native XPath query through the driver's accessibility tree |
2 |
| Direct text / CSS / class lookup through the element source |
3 |
| Screenshot → OCR (EasyOCR, Pytesseract, Google Vision, remote OCR) |
4 |
| Screenshot → template matching against a reference PNG |
5 | AI self-heal (opt-in) | All four failed → an LLM reads the screen and recovers |
Cheap strategies run first, so vision only costs you time when the tree can't help. Steps 1–4 are LocatorStrategy registrations; step 5 is a separate recovery layer, bounded to five turns and a six-keyword allowlist so it re-enters the ladder rather than tapping blind coordinates. Two more fallback axes sit alongside: multiple values per element name, and multiple enabled drivers or element sources, each tried in config order.
Beyond the ladder:
Tests are data — elements, modules and test cases as plain CSV or YAML. No IDE, no programming.
Targets — Android, iOS, web (Selenium/Playwright), Android TV, Samsung Tizen, LG webOS.
Non-intrusive — the
bledriver drives production devices as a Bluetooth HID mouse/keyboard where debugging and screenshots are blocked. Coordinate-only, so pair it withcamera_screenshotand the vision strategies.Agent-ready —
optics mcpexposes every keyword as a typed MCP tool and device state as MCP resources.
Write a test as data
my_test_project/
├── config.yaml
├── test_cases/test_cases.csv
├── modules/modules.csv
└── test_data/
├── elements.csv
├── error_definitions.csv # optional
└── input_templates/*.png # optional, for image matchingOptics discovers these files by their content (CSV headers / YAML top-level keys), not their path — the folder names above are a convention, so elements.csv works equally under test_data/ or elements/ (the folder optics live's /save writes to).
test_data/elements.csv — names mapped to locators. Doubles as a general variable store; repeating a name builds a fallback list.
Element_Name,Element_ID
Add_Contact_Button,//android.widget.Button[@content-desc="Create contact"]
First_Name_element,//android.widget.EditText[@text="First name"]
Save_Button,Save
First_Name,JohnA locator can be an XPath, text=…, css=…, a plain string, an image filename from input_templates/, or TEXT_ONLY:… to force a vision-based search.
modules/modules.csv — a reusable sequence of keywords; ${name} resolves against elements.csv.
module_name,module_step,param_1,param_2
Add Contact,Press Element,${Add_Contact_Button}
Add Contact,Enter Text,${First_Name_element},${First_Name}
Add Contact,Press Element,${Save_Button}test_cases/test_cases.csv — modules sequenced into scenarios. A test case whose name contains suite + setup (or teardown) is hoisted to run around the whole suite.
test_case,test_step
Suite Setup,Launch Contact Application
Add Contact with Contact App,Add Contact
Add Contact with Contact App,Verify Contact is AddedSix ways to run the same keywords
Surface | Command / import | Best for |
CLI runner |
| CI suites written as CSV/YAML |
Interactive TUI |
| Building a test by doing it — recording is always on, |
Python SDK |
| Custom logic, embedding in existing suites |
Robot Framework |
| Teams already on Robot |
REST API |
| Remote/orchestrated execution, live workspace streaming over SSE |
MCP server |
| Letting an AI agent drive a real device |
Every successful keyword is buffered as you work. To persist the buffer:
/save <test_case> <module_name>That appends the recorded keywords to modules/modules.csv as <module_name>, adds a (<test_case>, <module_name>) row to test_cases/test_cases.csv, creates a header-only elements/elements.csv stub if none exists, and copies the session's screenshots to execution_output/<module_name>/. The buffer then clears, so the next actions become the next module. If either name already exists, re-run the identical /save to confirm the append.
Other commands: /device [id], /elements, /screenshot, /help, /quit. Full reference: Live Usage.
from optics_framework import Optics
optics = Optics()
optics.setup(
driver_sources=[{"appium": {"enabled": True, "url": "http://localhost:4723"}}],
elements_sources=[{"appium_find_element": {"enabled": True}}],
)
optics.launch_app(app_identifier="com.example.app")
optics.enter_text("username_field", "testuser")
optics.press_element("submit_button")
optics.validate_element("welcome_message")
optics.quit(){ "mcpServers": { "optics": { "command": "optics", "args": ["mcp"] } } }Then: start_session → observe (screenshot, optics://session/{id}/source) → act (press_element, enter_text, …) → terminate_session. For networked use: optics mcp --transport http --port 8090. Sessions are not shared with optics serve — each is its own process.
Keywords
Every public method on the four API classes is automatically a keyword, on every surface above. CSV/YAML uses Title Case (Press Element → press_element).
Category | Keywords |
Actions | Press Element · Press By Percentage · Press By Coordinates · Detect And Press · Select Dropdown Option · Swipe · Swipe By Percentage · Swipe From Element · Swipe Until Element Appears · Scroll · Scroll From Element · Scroll Until Element Appears · Enter Text · Enter Text Direct · Enter Text Using Keyboard · Enter Number · Clear Element Text · Press Keycode · Get Text · Sleep · Execute Script |
Verification | Assert Presence · Assert Visibility · Assert Equality · Validate Element · Validate Screen · Is Element · Get Interactive Elements · Get Screen Elements · Capture Screenshot · Capture Pagesource |
App lifecycle | Launch App · Launch Other App · Start Appium Session · Get Driver Session Id · Close And Terminate App · Force Terminate App · Get App Version |
Flow control | Run Loop · Condition · Read Data · Evaluate · Date Evaluate · Invoke API |
Run optics list for the live catalogue with signatures, or read the Keyword Usage guide for parameters and examples. Location keywords accept percentage-based Area-of-Interest bounds (aoi_x/y/width/height, 0–100) to scope a vision search to part of the screen.
Press Checkbox and Press Radio Button still resolve but are deprecated aliases of Press Element — use Press Element directly. Add API is available on the Optics Python class only, not to the CSV/YAML runner; define APIs in an api.yaml and call them with Invoke API instead.
Configure once, in config.yaml
Every section is a priority-ordered list and every entry has an enabled flag. Enable a second driver and it becomes a fallback. Prefer not to hand-write it? optics configure <folder> writes one from a few questions, or optics quickstart builds the whole project including the config.
driver_sources:
- appium:
enabled: true
url: "http://localhost:4723"
capabilities:
platformName: Android
automationName: UiAutomator2
deviceName: emulator-5554
appPackage: com.google.android.contacts
appActivity: com.android.contacts.activities.PeopleActivity
elements_sources:
- appium_find_element: { enabled: true }
- appium_page_source: { enabled: true }
- appium_screenshot: { enabled: true }
text_detection:
- easyocr: { enabled: true }
image_detection:
- templatematch: { enabled: false }
log_level: INFOLayer | Available engines |
Drivers |
|
Element sources |
|
Text detection |
|
Image detection |
|
LLM |
|
Enabling the LLM features
The llm_models block powers both natural-language mode in optics live (Ctrl-N) and AI self-heal. Pick the engine that matches the account you already have, install its extra, and enable it:
You have | Install | Enable |
A Gemini API key, or Vertex AI |
|
|
An OpenAI key |
|
|
An OpenRouter key (any hosted model) |
|
|
A local model (Ollama, vLLM, LM Studio) |
|
|
A Claude key, or Claude on Bedrock / Vertex |
|
|
llm_models:
- openai:
enabled: true
url: https://openrouter.ai/api/v1 # omit for OpenAI itself
capabilities:
model: anthropic/claude-sonnet-5-5
api_key_env: OPENROUTER_API_KEY # default OPENAI_API_KEY
ai_self_heal: true # opt into the LLM backstop; default falseKeys always come from environment variables (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, or the variable api_key_env names). Never commit keys to config.yaml. Enable more than one entry and Optics falls back to the next when a call fails. A misconfigured LLM degrades to "no self-heal" rather than a hard failure.
Full reference: Configuration. Adding your own engine is a file drop plus an interface — see Extending the Framework.
Results
An optics execute run writes to <project>/execution_output/:
junit_output.xml— written incrementally, so CI sees progress as it happenslogs.json— structured logs whenjson_log: truescreenshots — pre/post action frames, plus strategy-annotated and AOI overlays
detected_errors_<session_id>.json— on-screen error detection
Drop an error_definitions.csv into test_data/ and Optics scans visible text for crash dialogs, Session expired, network errors, and the like — no assertions required. Matches also land in the JUnit XML as a synthetic failing testcase, so CI fails a build on "the app crashed mid-test" the same way it fails a normal assertion. See Error Detection.
CLI reference
optics init Scaffold a new project (positional NAME; --template, --path, --force, --git-init)
optics quickstart Guided walkthrough: engines + project + config + doctor check
optics configure Write/edit a project's config.yaml (guided prompts; --edit opens a TUI)
optics doctor Diagnose engines, tooling and a project's config (--check for CI)
optics setup Install engine backends (--list, --install); bare command opens a TUI
optics dry_run Validate a project without touching a device
optics execute Run a project (--runner test_runner|pytest)
optics live Interactive keyword session against a live target
optics generate Emit pytest or Robot Framework code from a project
optics list Print every discoverable keyword
optics serve Start the REST API server (--host, --port, --workers)
optics mcp Start the MCP server (--transport stdio|http)
optics completion Install shell autocompletion
optics --version Print the installed versionDetails in the CLI guide.
Contributing
Contributions welcome — bug reports, docs, and code are all first-class.
Bigger items are queued under
help wanted.Found a bug or missing a keyword? Open an issue.
The contribution guide covers development setup, step-by-step recipes (adding a keyword or engine backend), commit conventions, and the pull-request format reviewers expect. Tooling details live in the Developer Guide; behaviour expectations in our Code of Conduct.
git clone git@github.com:mozarkai/optics-framework.git
cd optics-framework
pipx install poetry
poetry install --with dev,test,docs
poetry run pytest # tests + coverage
poetry run ruff check --fix . # lint
poetry run pre-commit run --all-files
poetry run mkdocs serve # docs previewCommits follow Conventional Commits, enforced by commitizen in the commit-msg hook.
Security issues follow SECURITY.md rather than public issues.
License & support
Apache 2.0 — see LICENSE.
Questions and bugs: GitHub Issues.
This server cannot be deployed
Maintenance
Related MCP Connectors
remote debug iOS/Android/Unity/Godot/Flutter/RN/Web on real-device.ui-tree/screenshots/taps,tests.
Drive real Android & iOS devices and web browsers from natural language for mobile + web QA. 290+ tools across device control, app management, automation sessions, browser automation, and flow recording / replay. Bearer-auth — get a token at robotactions.com → Profile → API Tokens.
Control real Android and iOS devices with LLM agents — tap, swipe, type, automate flows.
Drive real devices from your AI Coding tool. Embed a client SDK (Unity, Godot, Flutter, iOS/macOS, Android, React Native, Web) in your app, then capture screenshots, traverse the UI tree, inject taps and key events, and run automated test tasks on the physical device over a secure relay.
Related MCP Servers
AlicenseAqualityAmaintenanceEnables AI assistants to automate mobile app testing and development for iOS and Android through natural language interactions. Supports intelligent element identification, session management, automated test generation, and comprehensive device interactions including clicks, swipes, screenshots, and app management.3210,465 npm498Apache 2.0- AlicenseAqualityAmaintenanceDrives an Android emulator or a physical device over adb. The agent screenshots, reads the UI hierarchy with true device-pixel coordinates, taps and types, manages app lifecycle, reads logcat, and runs Gradle builds and tests.797MIT
- AlicenseNot gradedqualityBmaintenanceEnables coding agents to verify work on live cloud devices by uploading builds, starting sessions, driving UI through UI-tree refs, reading logs, and taking screenshots on iOS, Android, Apple TV, and Android TV.MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to automate Android and iOS devices, including emulators and physical devices, by capturing screenshots, reading UI accessibility trees, performing touch and text input, and managing app lifecycle and logs.MIT