| name | cdp-bridge-mcp-browser-control |
| description | Control and automate real browser sessions through CDP, preserving login state and cookies for LLM-driven interactions |
| triggers | ["connect to my browser tabs","execute JavaScript in the current page","scan the current webpage content","take a screenshot of this page","get cookies from the browser","navigate to a URL in the browser","switch between browser tabs","wait for page element to load"] |
CDP Bridge MCP Browser Control
Skill by ara.so — MCP Skills collection.
Overview
CDP Bridge MCP is a Model Context Protocol server that bridges LLM clients to real, active browser sessions through Chrome DevTools Protocol (CDP) and a companion browser extension. Unlike headless automation tools, it connects to your already-open, already-logged-in browser tabs, preserving authentication state, cookies, and rendered page content.
Key differentiators:
- Reuses real login sessions — no need to re-authenticate or transfer cookies
- Works with current browser state — connects to tabs you already have open
- LLM-optimized page scanning — filters HTML to preserve useful content while reducing tokens
- Automatic CSP handling — falls back to CDP when content security policies block script injection
- Lightweight setup — no separate browser instances or complex configuration
Installation
1. Install the MCP Server
The server is available via PyPI and can be run with uvx:
uvx cdp-bridge@latest
uvx cdp-bridge@latest --transport streamable-http --port 8000
2. Load Browser Extension
- Navigate to
chrome://extensions/
- Enable "Developer mode"
- Click "Load unpacked"
- Select the
src/cdp_bridge/tmwd_cdp_bridge folder from the repository
The extension automatically attempts to connect to 127.0.0.1:18765 (WebSocket) and retries every ~5 seconds if the server isn't running yet.
3. Configure MCP Client
For stdio mode (Claude Desktop, most local clients):
{
"mcpServers": {
"cdp-bridge": {
"command": "uvx",
"args": ["cdp-bridge@latest"]
}
}
}
For streamable-http mode (shared/Docker deployments):
First start the server:
uvx cdp-bridge@latest --transport streamable-http --port 8000
Then configure the client:
{
"mcpServers": {
"cdp-bridge": {
"type": "streamableHttp",
"url": "http://127.0.0.1:8000/mcp"
}
}
}
Claude Code:
claude mcp add cdp-bridge uvx cdp-bridge@latest
claude mcp add cdp-bridge --transport streamable-http http://127.0.0.1:8000/mcp
Codex:
codex mcp add cdp-bridge uvx cdp-bridge@latest
codex mcp add cdp-bridge --transport streamable-http --url http://127.0.0.1:8000/mcp
Available Tools
| Tool | Purpose |
|---|
browser_get_tabs | List all connected browser tabs with URLs and titles |
browser_scan | Extract page content as simplified HTML or plain text |
browser_execute_js | Run JavaScript in the active tab |
browser_switch_tab | Change which tab is active for MCP operations |
browser_batch | Execute multiple CDP/extension commands atomically |
browser_wait | Poll a JavaScript condition until true or timeout |
browser_navigate | Navigate the active tab to a URL |
browser_screenshot | Capture page screenshot as base64 PNG |
browser_cookies | Read cookies for the current domain |
Common Usage Patterns
Get Available Tabs
result = await use_mcp_tool(
server_name="cdp-bridge",
tool_name="browser_get_tabs",
arguments={}
)
Scan Page Content
result = await use_mcp_tool(
server_name="cdp-bridge",
tool_name="browser_scan",
arguments={
"tab_id": "tab_0",
"format": "html"
}
)
How browser_scan works:
- Removes
<script>, <style>, <svg>, hidden elements
- Keeps text content, links, headings, form controls
- Converts
<img> to [Image: alt_text]
- Ideal for reducing token usage while preserving page semantics
Execute JavaScript
result = await use_mcp_tool(
server_name="cdp-bridge",
tool_name="browser_execute_js",
arguments={
"tab_id": "tab_0",
"code": """
return {
title: document.title,
links: Array.from(document.querySelectorAll('a'))
.slice(0, 10)
.map(a => ({href: a.href, text: a.innerText}))
};
"""
}
)
Execution modes:
- Primary: Uses
chrome.scripting.executeScript in MAIN world
- Fallback: Uses CDP
Runtime.evaluate if CSP blocks injection
Wait for Dynamic Content
result = await use_mcp_tool(
server_name="cdp-bridge",
tool_name="browser_wait",
arguments={
"tab_id": "tab_0",
"condition": "document.querySelector('.dynamic-content') !== null",
"timeout": 10,
"interval": 0.5
}
)
Navigate and Screenshot
await use_mcp_tool(
server_name="cdp-bridge",
tool_name="browser_navigate",
arguments={
"tab_id": "tab_0",
"url": "https://example.com"
}
)
await use_mcp_tool(
server_name="cdp-bridge",
tool_name="browser_wait",
arguments={
"tab_id": "tab_0",
"condition": "document.readyState === 'complete'",
"timeout": 30
}
)
screenshot = await use_mcp_tool(
server_name="cdp-bridge",
tool_name="browser_screenshot",
arguments={
"tab_id": "tab_0",
"format": "png"
}
)
Read Cookies
result = await use_mcp_tool(
server_name="cdp-bridge",
tool_name="browser_cookies",
arguments={
"tab_id": "tab_0"
}
)
Batch Operations (Advanced)
result = await use_mcp_tool(
server_name="cdp-bridge",
tool_name="browser_batch",
arguments={
"tab_id": "tab_0",
"commands": [
{
"method": "Runtime.evaluate",
"params": {
"expression": "document.title",
"returnByValue": True
}
},
{
"method": "Network.getCookies",
"params": {}
}
]
}
)
Real-World Examples
Extract Article Content from Logged-In Site
tabs = await use_mcp_tool("cdp-bridge", "browser_get_tabs", {})
article_tab = next(t for t in tabs["tabs"] if "medium.com" in t["url"])
await use_mcp_tool("cdp-bridge", "browser_switch_tab", {"tab_id": article_tab["id"]})
content = await use_mcp_tool(
"cdp-bridge",
"browser_execute_js",
{
"tab_id": article_tab["id"],
"code": """
const article = document.querySelector('article');
return article ? article.innerText : 'No article found';
"""
}
)
Monitor Dashboard Data
while True:
metrics = await use_mcp_tool(
"cdp-bridge",
"browser_execute_js",
{
"tab_id": "tab_0",
"code": """
return {
visitors: document.querySelector('.visitor-count')?.innerText,
revenue: document.querySelector('.revenue')?.innerText,
timestamp: Date.now()
};
"""
}
)
await asyncio.sleep(30)
Fill Form in Authenticated Session
await use_mcp_tool(
"cdp-bridge",
"browser_navigate",
{"tab_id": "tab_0", "url": "https://example.com/settings"}
)
await use_mcp_tool(
"cdp-bridge",
"browser_wait",
{
"tab_id": "tab_0",
"condition": "document.querySelector('form#settings') !== null",
"timeout": 10
}
)
await use_mcp_tool(
"cdp-bridge",
"browser_execute_js",
{
"tab_id": "tab_0",
"code": """
const form = document.querySelector('form#settings');
form.querySelector('input[name="email"]').value = 'user@example.com';
form.querySelector('input[name="notifications"]').checked = true;
form.submit();
"""
}
)
Troubleshooting
Extension Shows "ERR_CONNECTION_REFUSED"
Normal behavior on first load. The extension retries connection every ~5 seconds. Once you invoke any MCP tool (e.g., browser_get_tabs), the server starts and the extension connects automatically within seconds.
No Tabs Appear in browser_get_tabs
- Verify the extension is loaded and enabled at
chrome://extensions/
- Check the extension's service worker console for connection status
- Ensure MCP server is running (invoke any tool to start it in stdio mode)
- Confirm WebSocket server is listening on
127.0.0.1:18765
JavaScript Execution Fails with CSP Error
The extension automatically falls back to CDP Runtime.evaluate when CSP blocks chrome.scripting. If both fail:
- Check browser console for specific CSP violations
- Try wrapping code in
(function() { ... })()
- Avoid accessing restricted APIs (e.g., cross-origin iframes)
Tab IDs Change Unexpectedly
Tab IDs are session-based and reset when:
- The extension reloads
- The browser restarts
- WebSocket reconnects
Always call browser_get_tabs before targeting a specific tab if state may have changed.
High Token Usage from browser_scan
- Use
"format": "text" for plain text extraction (fewer tokens)
- Limit scope by executing JS to extract specific DOM subtrees first:
await use_mcp_tool(
"cdp-bridge",
"browser_execute_js",
{
"tab_id": "tab_0",
"code": "document.body.innerHTML = document.querySelector('main').innerHTML"
}
)
await use_mcp_tool("cdp-bridge", "browser_scan", {"tab_id": "tab_0"})
Configuration Options
Server Launch Arguments
uvx cdp-bridge@latest [OPTIONS]
Options:
--transport [stdio|streamable-http] Transport mode (default: stdio)
--port INTEGER HTTP port for streamable-http mode (default: 8000)
Extension Configuration
Edit src/cdp_bridge/tmwd_cdp_bridge/background.js to customize:
const WS_URL = 'ws://127.0.0.1:18765';
const RECONNECT_INTERVAL = 5000;
Architecture Notes
- Transport modes:
stdio for single-client (subprocess), streamable-http for multi-client (persistent server)
- Extension communication: WebSocket (primary) + HTTP long-polling (fallback)
- JavaScript execution: MAIN world via
chrome.scripting, falls back to CDP for CSP-restricted pages
- Session management: Each connected tab gets a unique session ID, managed by
TMWebDriver
Requirements
- Python 3.10+
- Chrome or Chromium-based browser
- Browser extension loaded in developer mode
- Network access to
127.0.0.1:18765 (WebSocket) and 127.0.0.1:18766 (HTTP)