Skip to main content
Glama
README.md
<p align="center">
  <img src="https://raw.githubusercontent.com/hunglp97/tabpilot-mcp/main/docs/assets/banner.png" alt="TabPilot Banner" width="100%" onerror="this.style.display='none'"/>
</p>

# 🧭 TabPilot

<p align="center">
  <strong>Drive the real Chrome you are already logged into β€” on macOS, Windows, and Headless Ubuntu 24/7.</strong>
</p>

<p align="center">
  <a href="https://pypi.org/project/tabpilot-mcp/"><img src="https://img.shields.io/pypi/v/tabpilot-mcp.svg?color=blue" alt="PyPI Version"></a>
  <a href="https://pypi.org/project/tabpilot-mcp/"><img src="https://img.shields.io/pypi/dm/tabpilot-mcp" alt="PyPI Downloads"></a>
  <a href="https://github.com/hunglp97/tabpilot-mcp/actions/workflows/ci.yml"><img src="https://github.com/hunglp97/tabpilot-mcp/actions/workflows/ci.yml/badge.svg" alt="CI Status"></a>
  <a href="https://glama.ai/mcp/servers/hunglp97/tabpilot-mcp"><img src="https://glama.ai/mcp/servers/hunglp97/tabpilot-mcp/badges/score.svg" alt="tabpilot-mcp MCP server – quality and maintenance score on Glama"></a>
  <a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-Standard%20Compatible-00C49F.svg" alt="MCP Compatible"></a>
  <a href="https://www.python.org"><img src="https://img.shields.io/badge/Python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-3776AB" alt="Python Versions"></a>
  <a href="https://github.com/hunglp97/tabpilot-mcp/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
  <a href="https://github.com/hunglp97/tabpilot-mcp/stargazers"><img src="https://img.shields.io/github/stars/hunglp97/tabpilot-mcp?style=social" alt="GitHub Stars"></a>
</p>

<p align="center">
  <a href="https://glama.ai/mcp/servers/hunglp97/tabpilot-mcp">
    <img src="https://glama.ai/mcp/servers/hunglp97/tabpilot-mcp/badges/card.svg" alt="tabpilot-mcp MCP server – quality and maintenance score on Glama">
  </a>
</p>

---

Most browser MCP servers launch a **fresh, blank browser instance**. That works for scraping static pages, but fails completely on tasks that matter:
- 🚫 **Cloudflare & Bot Shields** immediately flag fresh automation browsers.
- 🚫 **Corporate SSO, Okta, & 2FA** make authenticating from scratch painful or impossible.
- 🚫 **Read-only tab viewers** can only *look* at DOM text, not click or fill forms.

**TabPilot bridges this gap.** It attaches directly to your **existing, logged-in Google Chrome**. Your AI agents (Claude, Cursor, Antigravity, Cline) can read pages with up to **99% token savings**, click elements with authentic mouse events (`isTrusted: true`), defeat React state caching traps, solve multi-row survey grids, and capture background screenshots without stealing window focus.

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  YOUR WORKSPACE                     TABPILOT MCP ENGINE           REAL BROWSER    β”‚
β”‚                                                                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ AI Agents            β”‚          β”‚ TabPilot MCP Server  β”‚      β”‚ Real Chrome  β”‚ β”‚
β”‚  β”‚ - Claude Desktop     β”‚  stdio / β”‚ - Token Budget Slicerβ”‚ CDP  β”‚ - Active SSO β”‚ β”‚
β”‚  β”‚ - Cursor IDE         β”‚ ───────> β”‚ - Native Form Setters│────> β”‚ - Cookies    β”‚ β”‚
β”‚  β”‚ - Antigravity/Gemini β”‚   SSH    β”‚ - Matrix Grid Driver β”‚<──── β”‚ - 2FA Saved  β”‚ β”‚
β”‚  β”‚ - Cline / Windsurf   β”‚          β”‚ - Dual CDP/AppleScr. β”‚      β”‚ - Real Fingerβ”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

---

## ⚑ Quick Start

### 1. Test your environment in one command
Run `tabpilot doctor` via `uvx` (no installation required):

```bash
uvx tabpilot-mcp doctor
```

*(Or install via pip: `pip install tabpilot-mcp`)*

### 2. Configure your MCP Client
Add to `claude_desktop_config.json` or `.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "tabpilot": {
      "command": "uvx",
      "args": ["tabpilot-mcp"]
    }
  }
}
```

### 3. Launch Chrome with Remote Debugging
Quit Chrome completely, then start it pointing to your persistent automation profile:

```bash
# macOS
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --remote-debugging-port=9222 --user-data-dir="$HOME/tabpilot-chrome"

# Linux
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/tabpilot-chrome"

# Windows
taskkill /F /IM chrome.exe
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
  --remote-debugging-port=9222 --user-data-dir="%USERPROFILE%\tabpilot-chrome"
```

> [!IMPORTANT]
> **🍎 macOS Zero-Config Fallback:**  
> On macOS, if you do not launch Chrome with debugging flags, TabPilot automatically falls back to AppleScript to drive the Chrome you already have open.  
> **Prerequisite:** In Google Chrome, go to **View β†’ Developer β†’ check "Allow JavaScript from Apple Events"**.

---

## πŸ’° How TabPilot Saves 90%+ Tokens

Naive browser automation dumps `document.body.innerText` or raw HTML, wasting 15,000–45,000 tokens on navigation bars, cookies banners, and tracking scripts.

TabPilot provides **Progressive Token Slicing**:

```
Raw Page Dump (eval_js body)  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 18,000+ tokens
read_tab() (Link-Density)     β–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ ~1,200 tokens (93% saved)
read_tab(selector)            β–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ ~350 tokens (98% saved)
query_dom(selector)           ▏                                      ~18 tokens (99.9% saved)
```

| Method | Tokens | Savings | Best For |
| :--- | :---: | :---: | :--- |
| **`query_dom(selector)`** | **~15–30** | **99.9%** | Checking button states (`disabled`), inputs, badges, or alerts |
| **`read_tab(selector)`** | **~300–600** | **98.0%** | Reading isolated articles, ticket cards, or form sections |
| **`read_tab()`** *(Link-Density)* | **~1,200–1,800** | **93.0%** | Full-page reads with nav, ads, headers, and footers stripped |
| ❌ *Naive `eval_js` innerText* | 15,000–35,000 | 0% | *Context budget incinerator* |

> πŸ’‘ **Enforced Character Budget:** Reads default to `--max-chars 20000`. If truncated, TabPilot alerts the model with exact cut sizes, prompting it to narrow down with `selector` instead of hallucinating.

---

## πŸ₯Š Driving Forms That Fight Back

Modern Single-Page Applications (React, Vue, Svelte) defeat standard automation scripts. TabPilot solves the 3 most infamous SPA traps:

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ TRAP 1: React Synthetic Cache                                                     β”‚
β”‚ Standard `.value = 'x'` is ignored by React's internal tracker.                   β”‚
β”‚ βœ… TabPilot calls native property setters + fires both `input` and `change`.      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ TRAP 2: Multi-Row Survey Grid Batching                                            β”‚
β”‚ Clicking 25 matrix rows in one JS tick causes React to commit only the last row.  β”‚
β”‚ βœ… `fill_matrix` clicks row-by-row across microtasks with delays, then confirms.  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ TRAP 3: Virtual Portals (React-Select, Headless UI)                               β”‚
β”‚ No `<select>` exists; options render only after clicking the trigger.             β”‚
β”‚ βœ… `select_option_ui` clicks trigger, awaits portal mount, and selects option.    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

---

## βš–οΈ Comparison

| Capability | Read-Only Tab Readers | Headless Bots (Puppeteer/Playwright) | 🧭 **TabPilot** |
| :--- | :---: | :---: | :---: |
| **Uses Existing Sessions & Cookies** | βœ… | ❌ Fresh empty profile | βœ… **Real Chrome (No re-login)** |
| **Bypasses Cloudflare & Bot Shields** | βœ… Human | ❌ Bot fingerprint | βœ… **Human browser fingerprint** |
| **Form Driving & Clicking** | ❌ Read-only | βœ… | βœ… **Full Bidirectional Driving** |
| **React Synthetic Event Fix** | ❌ | ⚠️ Often missed | βœ… **Native prototype setters** |
| **Survey Matrix Handler** | ❌ | ❌ Batched drops | βœ… **`fill_matrix` task scheduling** |
| **Token-Optimized Extraction** | ⚠️ Basic text | ❌ Raw DOM / costly vision | βœ… **`query_dom` (18 tokens)** |
| **Background Tab Screenshots** | ❌ Must be active | βœ… | βœ… **Off-screen CDP captures** |
| **Headless Ubuntu 24/7 Daemon** | ❌ macOS only | ⚠️ Complex Docker | βœ… **Native systemd stack** |
| **External Dependencies** | Minimal | ❌ Heavy Node/Playwright binaries | βœ… **Zero dependencies beyond MCP** |

---

## 🐧 24/7 Headless Ubuntu Server Stack

TabPilot is engineered to run permanently on cloud VPS servers (AWS, Hetzner, DigitalOcean) with zero exposed ports:

```
[ Laptop / Client ] ──( Encrypted SSH Pipe )──> [ Remote Ubuntu Server ]
                                                        β”‚
                                                        β–Ό
                                             [ TabPilot MCP Server ]
                                                        β”‚ (CDP 127.0.0.1:9222)
                                                        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Managed Systemd User Stack (Restart=always, Linger Enabled)                       β”‚
β”‚   tabpilot-xvfb.service   ──>   tabpilot-wm.service   ──>   tabpilot-chrome.service
β”‚   (Virtual Framebuffer)         (Openbox Window Mgr)        (Real Chrome + SSO)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

1. **One-Command Setup:** `bash deploy/ubuntu/install.sh` configures Xvfb, Openbox, Chrome, and systemd services with `Restart=always`.
2. **Font Protection (Anti-Tofu):** Headless servers often render Vietnamese and CJK characters as empty square boxes. `install.sh` installs Noto and Liberation fonts; `doctor` validates text rendering.
3. **Cgroup Memory Caps:** Prevent Chrome from exhausting host RAM:
   ```bash
   tabpilot install-stack --memory-high 4G
   ```
4. **Zero Remote Attack Surface:** Chrome CDP stays locked to `127.0.0.1`. Remote MCP connections run securely through SSH pipes (`ssh you@server "tabpilot serve"`).

---

## 🧰 Tools & Resources Reference

TabPilot exposes **17 tools** and **2 live resources**:

### πŸ” Reading & DOM Inspection
- **`query_dom(selector, attrs, limit, visible_only)`**: Atomic element inspection (~18 tokens). Checks disabled, checked, values.
- **`read_tab(selector, mode, max_chars, url_pattern)`**: Link-density markdown extraction with token caps.
- **`list_tabs(url_pattern)`**: Lists open tabs with IDs, titles, and URLs (regex filterable).
- **`eval_js(expression, timeout_ms)`**: Runs arbitrary JS in tab context; automatically awaits Promises in CDP.

### 🧭 Navigation & Tabs
- **`open_tab(url, activate, wait_for_load)`**: Opens a URL in a new tab, awaiting page completion.
- **`close_tab(url_pattern, tab_id)`**: Closes tab and polls until process confirms destruction.
- **`navigate(url, wait_for_load)`**: Points tab to a new URL and awaits document load.
- **`activate_tab(url_pattern, tab_id)`**: Brings target tab to foreground focus.

### πŸ–±οΈ Interaction & Complex Forms
- **`click(selector, text, nth)`**: Scrolls element into view and emits trusted mouse event (`isTrusted: true`).
- **`fill(selector, value, clear, press_enter)`**: Sets form inputs via native prototype setters to trigger React/Vue.
- **`select_option(selector, values, by)`**: Selects options in native `<select>` or Select2 dropdowns.
- **`select_option_ui(control_selector, option_text)`**: Clicks trigger, waits for popup portal, and selects item.
- **`wait_for(selector, state, text, timeout_ms)`**: Polls until condition holds (`visible`, `hidden`, `text`, `enabled`).

### πŸ“‹ Matrix Surveys & Evidence
- **`scan_matrix(selector)`**: Discovers multi-row grid questions and flags unanswered rows.
- **`fill_matrix(column_index, rows, delay_ms)`**: Answers matrix rows sequentially with task delays.
- **`screenshot(full_page, selector, label)`**: Offscreen capture of any tab (local path or inline base64).
- **`browser_status()`**: Reports active backend, capabilities, CDP endpoint, and open tab count.

### πŸ“¦ Live MCP Resources
- `tab://active` β€” Markdown stream of the currently focused tab.
- `tab://{tab_id}` β€” Markdown stream of any specific tab by ID.

---

## πŸ› οΈ CLI Reference

```bash
tabpilot doctor                # Run diagnostic suite (Chrome, CDP, fonts, permissions)
tabpilot tabs                  # List open browser tabs in terminal
tabpilot serve                 # Start MCP server on stdio

# Linux Managed Stack Commands
tabpilot install-stack         # Install systemd user services
tabpilot up / tabpilot down    # Start or stop the headless stack
tabpilot status                # Check systemd stack status
tabpilot logs chrome -f        # Follow live Chrome logs
```

---

## πŸ§ͺ Testing

```bash
git clone https://github.com/hunglp97/tabpilot-mcp.git
cd tabpilot-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

# Unit tests (Zero browser needed)
pytest -v

# Live integration tests (against Chrome on port 9222)
pytest -v -m live
```

---

## πŸ“„ License

Distributed under the **MIT License**. See [LICENSE](LICENSE) for details.

---

<p align="center">
  Built with ❀️ for AI Engineers, QA Automators, and Power Users who demand real browser agency.
  <br>
  <strong>Star ⭐ TabPilot on GitHub if it saved your agents from bot shields and token burn!</strong>
</p>

TDQS

A4.2/5.0

Scored across 17 tools

Disambiguation5/5

Each tool targets a distinct operation: reading page content (read_tab) vs inspecting specific elements (query_dom) vs running custom JS (eval_js), and interaction tools (click, fill, select_option, select_option_ui) have clear boundaries. Even similar tools like scan_matrix/fill_matrix and select_option/select_option_ui are explicitly distinguished by widget type and purpose.

Naming Consistency4/5

Most names follow a verb_noun pattern (list_tabs, read_tab, open_tab, activate_tab, fill_matrix), but a few deviate: navigate, click, fill, and screenshot are single verbs or nouns used as verbs, while browser_status and wait_for use different structures. The pattern is still predictable and readable.

Tool Count4/5

With 17 tools, the set is slightly larger than the ideal 3–15 range but each tool serves a concrete browser automation function (tab management, reading, interaction, waiting, screenshots, matrix handling). The count feels justified for the domain, not bloated.

Completeness4/5

The tool surface covers tab lifecycle (open, close, list, navigate), multiple read modes (markdown, DOM inspection, screenshots), form interaction (fill, click, select, matrix-specific tools), and synchronization (wait_for). Minor gaps exist like no explicit back/forward or keyboard shortcuts, but these can be worked around with navigate or eval_js.

Maintenance

ActivityMaintained
ResponsivenessNo issues