Skip to main content

Helios

Helios is a Chrome extension + MCP server that enables AI-driven browser automation on your actual logged-in browser. Unlike Puppeteer/Playwright (which spawn new browser instances) or BrowserMCP (closed source, unreliable), Helios is:

  • Open source - Fully auditable extension and server
  • Token efficient - Structured DOM representation, not screenshot spam
  • Multi-tab aware - Control and coordinate across tab groups
  • Reliable - Native Messaging for rock-solid connections

Architecture

┌─────────────────┐                    ┌─────────────────┐
│ Claude Code │ stdio │ MCP Server │
│ / Any MCP Host │◄──────────────────►│ (Node.js) │
└─────────────────┘ └─────────────────┘

│ WebSocket
│ localhost:9333

┌─────────────────┐ Native Msg ┌─────────────────┐
│ Chrome Extension│◄──────────────────►│ Native Host │
│ (Manifest V3) │ stdio │ (Node.js) │
└─────────────────┘ └─────────────────┘

│ chrome.* APIs

┌─────────────────┐
│ Your Browser │
│ (logged in) │
└─────────────────┘

Why Native Messaging? Manifest V3 service workers are ephemeral - Chrome kills them after ~30 seconds of inactivity. Direct WebSocket from service worker = unreliable. Native Messaging solves this because:

  • Chrome manages the native host lifecycle
  • Native host maintains persistent WebSocket to MCP server
  • Extension reconnects instantly when service worker wakes
  • This is how 1Password, Bitwarden, and other serious extensions work

Repository

GitHub: aic-holdings/helios

Installation

1. Clone and build

git clone git@github.com:aic-holdings/helios.git
cd helios
npm install

# Build all packages
cd packages/server && npm install && npm run build && cd ..
cd packages/native-host && npm install && npm run build && cd ..

2. Install the Native Messaging Host

cd packages/native-host

# Find your extension ID first (see step 3), then:
./install.sh YOUR_EXTENSION_ID

# Or for development with wildcard (less secure):
./install.sh

This creates a manifest at ~/.config/google-chrome/NativeMessagingHosts/com.helios.native.json

3. Load the Chrome extension

  1. Open Chrome and go to chrome://extensions
  2. Enable "Developer mode" (top right)
  3. Click "Load unpacked"
  4. Select the packages/extension folder
  5. Copy the extension ID (shown under the extension name)
  6. Re-run install.sh with your extension ID

4. Configure Claude Code

Add to your Claude Code MCP settings (~/.claude.json or project .mcp.json):

{
"mcpServers": {
"helios": {
"command": "node",
"args": ["/path/to/helios/packages/server/dist/index.js"]
}
}
}

5. Verify connection

Click the Helios extension icon in Chrome - it should show "Connected" when the MCP server is running.

Available Tools

ToolDescription
pingTest connection to browser extension
tabs_listList all open browser tabs with IDs, URLs, titles
navigateNavigate a tab to a URL (active tab if not specified)
clickClick element by CSS selector or x,y coordinates
typeType text into input/textarea elements
read_pageGet structured DOM with interactive elements (~70% fewer tokens than screenshots)
screenshotCapture visible area on-demand (use sparingly - prefer read_page)
console_logsRead browser console output (injects interceptor on first call)
evaluateExecute JavaScript in page context (blocked by CSP on some sites)

Tool Examples

Navigate to a page:

navigate url="https://github.com"

Read page structure (token-efficient):

read_page
→ Returns: {url, title, elements: [{ref: "e1", tag: "button", text: "Sign in"}, ...]}

Click an element:

click selector="button.sign-in"
click x=100 y=200

Type into an input:

type selector="input[name='q']" text="search query"

Screenshot (use sparingly):

screenshot
→ Returns: image of visible tab

Read console logs:

console_logs tabId=123 level="error"
→ Returns: {logs: [{level, timestamp, args}, ...], interceptorActive: true}

Execute JavaScript:

evaluate tabId=123 code="document.querySelectorAll('a').length"
→ Returns: {value: 42, type: "number"}

Token Efficiency

Traditional browser automation sends a screenshot after every action (~1500 tokens each). Helios uses structured DOM representation:

{
"url": "https://duckduckgo.com/",
"title": "DuckDuckGo - Protection. Privacy. Peace of mind.",
"elements": [
{"ref": "e1", "tag": "a", "text": "Duck.ai", "href": "https://duck.ai/..."},
{"ref": "e7", "tag": "input", "type": "text", "name": "q", "placeholder": "Search privately"},
{"ref": "e8", "tag": "button", "text": "", "disabled": true}
],
"totalInteractive": 142,
"truncated": true
}

Tested result: DuckDuckGo homepage returns ~400 tokens vs ~1500 for screenshot. ~70% token reduction.

Comparison

FeatureHeliosBrowserMCPClaude in ChromePuppeteer
Uses actual browser
Keeps logged-in state
Open source❌ (extension closed)
Token efficientN/A
Reliable connection✅ (Native Msg)?N/A
Multi-tab supportLimited
Buildable standaloneN/A

Development

# Watch mode for server changes
cd packages/server && npm run dev

# Watch mode for native host changes
cd packages/native-host && npm run dev

# Reload extension after changes
# Go to chrome://extensions and click refresh on Helios

Troubleshooting

Extension shows "Disconnected"

  1. Make sure MCP server is running: node packages/server/dist/index.js
  2. Check native host is installed: cat ~/.config/google-chrome/NativeMessagingHosts/com.helios.native.json
  3. Verify extension ID matches in the manifest's allowed_origins
  4. Check service worker console for errors (chrome://extensions → Helios → "Service worker")

MCP tools return "Extension not connected"

  1. Click the Helios extension icon to check status
  2. Try clicking "Connect" in the popup
  3. Check that the native host can run: ./packages/native-host/helios-native-host (should connect to server)
  4. Restart Claude Code to reconnect MCP server

Native host not found

# Re-run installation with your extension ID
cd packages/native-host
./install.sh YOUR_EXTENSION_ID

# Verify manifest exists
cat ~/.config/google-chrome/NativeMessagingHosts/com.helios.native.json

evaluate tool blocked by CSP

Some sites have strict Content Security Policy that blocks eval(). This is expected behavior on security-conscious sites. Use read_page and click/type instead.