Four MCP servers that drive your real tooling from Claude Code — Chrome (via CDP), Android devices (via UIAutomator2), macOS desktop apps (via AX + ScreenCaptureKit), and Windows desktop apps (via UI Automation + SendInput). Same architecture, same distribution: semantic locators, live pause/inject, flow record/replay, one-line install.
Distributed via chrome-mcp.actuallyroy.com. The site hosts both bundled servers + tiny zero-dep loaders that handle install, updates, and SHA-256 tamper detection.
One step. Paste into ~/.claude.json (or .mcp.json in your project) and restart Claude Code. The exact block is on the landing page with the correct bootstrap inlined. In shape:
{
"mcpServers": {
"chrome": {
"command": "node",
"args": ["-e", "<short bootstrap that fetches loader.mjs on first run>"]
}
}
}Requires Node ≥18.
What happens:
- First launch: bootstrap downloads
~/.chrome-mcp/loader.mjs, loader fetches the latest bundle, verifies its SHA-256, caches it, executes it. - First tool call: MCP tries to connect to
localhost:9222— not there? It spawns Chrome with--remote-debugging-port=9222on a dedicated profile at~/ChromeMCP-Profile. A Chrome window pops up. - You sign into whatever sites once. The profile persists.
- Subsequent runs: Chrome is already up, bundle is cached, tools just work.
The dedicated profile coexists with your normal Chrome — we never touch your main profile.
Power-user install (skips the bootstrap, writes the loader directly):
curl -fsSL https://chrome-mcp.actuallyroy.com/install.sh | sh # macOS / Linux
irm https://chrome-mcp.actuallyroy.com/install.ps1 | iex # WindowsThis gives you ~/.chrome-mcp/bin/chrome-mcp as a persistent binary you can reference directly in .mcp.json instead of the node -e form. Only reason to prefer this: faster cold start (skips the bootstrap's fetch check).
The loader checks the endpoint on every launch, downloads a newer bundle if one exists, verifies its SHA-256, and uses the cached copy if the network is unreachable. Controls (env vars):
CHROME_MCP_PIN_VERSION=0.2.0— pin to a specific version, skip update checksCHROME_MCP_SKIP_UPDATE=1— use cached bundle, skip networkCHROME_MCP_ENDPOINT=https://…— override the origin (for self-hosting)CHROME_MCP_CACHE_DIR=/path— override the cache location
Locator-taking tools accept { ref, text, label, selector } — refs come from outline and stay stable across calls.
- Inspection:
outline,describe,screenshot,snapshot,get_text,get_html,get_url,get_title,get_attribute - Interaction:
click,fill,fill_form,select_option,press,type,hover,scroll - Capture:
get_toasts,wait_for_toast,get_console,get_network - Navigation:
navigate,go_back,go_forward,reload,wait_for_navigation,wait_for_selector - Tabs:
list_tabs,select_tab,new_tab,close_tab - Debug:
pause,resume,inject_script,evaluate - Flows:
start_recording,stop_recording,recording_status,run_script,assert - Cookies:
get_cookies,set_cookies
See examples/demo.flow.json for a sample run_script flow.
.
├── app/ # Next.js landing page + /api/version
├── loader/
│ ├── loader.mjs # chrome-mcp loader (zero-dep)
│ ├── bootstrap.js # chrome inline bootstrap for .mcp.json
│ ├── android-loader.mjs # android-mcp loader (zero-dep)
│ └── android-bootstrap.js # android inline bootstrap
├── installer/ # install.sh / install.ps1 (chrome power-user)
├── mcp-server/ # chrome-mcp TypeScript source
│ ├── src/
│ └── package.json
├── android-mcp/ # android-mcp TypeScript source
│ ├── src/
│ │ ├── adb.ts # adb subprocess wrapper
│ │ ├── devices.ts
│ │ ├── uiautomator2.ts # JSON-RPC client + APK install
│ │ ├── outline.ts # view-hierarchy → text outline + stable refs
│ │ ├── locators.ts # text/desc/id/xpath/ref resolvers
│ │ ├── logcat.ts # ring-buffered logcat capture
│ │ ├── tools.ts
│ │ ├── recorder.ts # copied from mcp-server/
│ │ └── index.ts
│ └── package.json
├── macos-mcp/ # macos-mcp Node + Swift sidecar
│ ├── src/
│ ├── swift-helper/ # AX + ScreenCaptureKit Swift sidecar
│ ├── scripts/build-helper.sh
│ └── package.json
├── windows-mcp/ # windows-mcp Node + C# sidecar
│ ├── src/
│ ├── csharp-helper/ # UI Automation + SendInput .NET 8 sidecar
│ ├── scripts/build-helper.ps1
│ └── package.json
├── scripts/
│ ├── build-mcp.mjs # builds chrome bundle + manifest
│ ├── build-android-mcp.mjs # builds android bundle + manifest + fetches UIAutomator2 APKs
│ ├── build-macos-mcp.mjs # builds macos bundle + manifest + Swift helper
│ ├── build-windows-mcp.mjs # builds windows bundle + manifest + C# helper
│ ├── launch-chrome.sh
│ └── launch-chrome.ps1
├── examples/
└── public/ # (gitignored; populated on build)
├── bundle/v<version>.mjs # chrome
├── bundle/manifest.json
├── loader.mjs # chrome loader
├── bootstrap.min.js
├── android/bundle/v<version>.mjs
├── android/bundle/manifest.json
├── android/loader.mjs
├── android/bootstrap.min.js
├── android/vendor/uiautomator2-server.apk
├── android/vendor/uiautomator2-server-test.apk
├── macos/bundle/v<version>.mjs
├── macos/bundle/manifest.json
├── macos/loader.mjs
├── macos/bootstrap.min.js
├── macos/vendor/macos-mcp-helper
├── windows/bundle/v<version>.mjs
├── windows/bundle/manifest.json
├── windows/loader.mjs
├── windows/bootstrap.min.js
└── windows/vendor/windows-mcp-helper.exe
Users must have adb on PATH or set ANDROID_MCP_ADB / $ANDROID_SDK_ROOT. Devices listed by adb devices are detected automatically; multiple devices require select_device first.
On first tool call, the MCP:
- Ensures exactly one device is active (or throws).
- Checks that
io.appium.uiautomator2.server+ its test APK are installed; if not, downloads from the Vercel endpoint andadb install -rs them. adb forward tcp:6790 tcp:6790.- Starts the UIAutomator2 test runner (
am instrument …), polls/wd/hub/statusuntil ready. - Creates a session, caches the id for the process lifetime.
Set ANDROID_MCP_APK_LOCAL=/abs/path/to.apk to use a hand-placed APK instead of downloading.
On some emulator AVD configurations (especially Play Store images, or anything that blocks AccessibilityService by policy), the Appium UIAutomator2 server reports IllegalStateException: UiAutomation not connected, UiAutomation@…[id=-1, ...] on session creation. The APK installs, the HTTP server starts on :6790, but the AccessibilityService handshake never completes. Workarounds:
- Use a Google APIs (non-Play Store) system image for the AVD; accessibility service hookup is permissive on those.
- Or: enable the server as an accessibility service explicitly:
adb shell settings put secure enabled_accessibility_services io.appium.uiautomator2.server.test/androidx.test.runner.AndroidJUnitRunner adb shell settings put secure accessibility_enabled 1
- Or: swap the driver to openatx/uiautomator2-server — its init path doesn't require AccessibilityService and is generally more emulator-friendly. This requires changing the APK references in
android-mcp/src/uiautomator2.ts(see issue #34 in project notes).
Everything up to the session handshake (ADB, APK install, instrumentation, HTTP ready) is validated; this is purely an Android-side accessibility-policy issue on certain devices.
npm install # root deps (Next.js + esbuild)
npm run build # runs build:mcp (tsc + esbuild + manifest), then next build
npm run dev # dev server only; run `npm run build:mcp` first
- Bump
mcp-server/package.json→version. - Commit + push. Vercel rebuilds: the manifest now points to
v<new>.mjs. - Existing installs auto-update on their next launch (unless they've set
CHROME_MCP_PIN_VERSIONorCHROME_MCP_SKIP_UPDATE).
cd mcp-server
npm install
npm run build
For iterating without going through Vercel, point a local install at a local server:
# Terminal 1: serve public/ on port 18888
npx http-server ./public -p 18888
# Terminal 2: run Claude Code with
CHROME_MCP_ENDPOINT=http://127.0.0.1:18888 ~/.chrome-mcp/bin/chrome-mcp
chrome-mcp and android-mcp are released under the GNU General Public License v3.0 (see LICENSE).
GPLv3 is a copyleft license. In short:
- ✅ Use it freely — personally, internally, for clients, in CI, anywhere. There is no usage restriction.
- ✅ Modify it freely.
⚠️ If you redistribute it (binaries, forks, repackaged installers, hosted services that ship the code to users), the redistributed work must also be GPLv3 and you must make the corresponding source available to recipients.⚠️ Code that links into / extends the MCP servers (custom tools compiled in, forks of the loader/bundle) inherits GPLv3.- ❌ You can't sublicense it under MIT/Apache/proprietary.
Bundled third-party deps (puppeteer-core, @modelcontextprotocol/sdk, pngjs, the Appium UIAutomator2 server APK) are Apache-2.0 / MIT and are GPLv3-compatible.
If you're unsure whether your use case requires source disclosure, the practical rule of thumb is: internal use never does; redistribution does.