How to Use Python Code Execution with OpenBrowser MCP for Web Automation
Purpose
This post demonstrates how to use Python code execution with OpenBrowser MCP for web automation. I will show you how to let your AI agent write Playwright scripts, execute them in a persistent browser runtime, and return only the specific data you need.
Environment
- OpenBrowser MCP (latest)
- Python 3.10+
- Playwright for Python
- Claude Desktop (or any MCP-compatible client)
The Problem: Traditional Browser Automation
When I tried to automate web scraping with most browser automation tools, I hit a wall. They expose dozens of specialized tools like navigate_to_url, click_element, fill_input, take_screenshot.
{ "tools": [ {"name": "navigate_to_url", "description": "Go to URL"}, {"name": "click_element", "description": "Click element"}, {"name": "fill_input", "description": "Fill text input"}, {"name": "submit_form", "description": "Submit form"} ]}The problem? I had to chain multiple tool calls to accomplish simple tasks. Even worse, these tools would dump the entire page HTML back to me, bloating the context window with 50KB of markup when I only needed 3 data points.
The OpenBrowser MCP Approach
OpenBrowser MCP takes a different approach. It exposes exactly one tool called run_python. My AI agent writes arbitrary Python code, and OpenBrowser executes it in a persistent runtime environment with full browser access.
I can explain the key parts:
- One tool: Agent writes Python code instead of calling specialized tools
- Persistent runtime: Browser state, sessions, and context stay alive across executions
- Controlled return: Agent decides what data comes back, no full page dumps
Here’s how it works:
AI Agent → Python Code → OpenBrowser MCP → Browser Runtime → Chrome/Firefox ↓ ↑ Clean JSON ←───────────────────────The key insight from the Reddit discussion: “Just the data agent asked for, no bloated page dumps.”
Basic Web Scraping Example
Let me show you how to scrape a product page. I want the title and price, nothing else.
from playwright.sync_api import sync_playwright
def scrape_product(url): """Extract title and price, return only what we need.""" with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page()
page.goto(url)
# Wait for product details to load page.wait_for_selector('.product-title')
# Extract ONLY the data we need title = page.locator('.product-title').text_content() price = page.locator('.price').text_content()
browser.close()
# Return structured data, not entire page return { 'title': title.strip(), 'price': price.strip(), 'url': url }
# Usageresult = scrape_product('https://example.com/product/123')# Returns: {'title': 'Widget Pro', 'price': '$29.99', 'url': '...'}I can explain what’s happening:
- Launch headless Chrome browser
- Navigate to the product URL
- Wait for the product title to appear
- Extract just the title and price text
- Return a clean JSON object
No 50KB HTML dump. No boilerplate. Just the data I asked for.
Persistent Session for Multi-Step Workflows
The real power is the persistent runtime. I can log in once, then reuse that session across multiple Python executions.
from playwright.sync_api import sync_playwright
# First call: Logindef login_and_get_dashboard(): global browser, page
with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context() page = context.new_page()
# Navigate to login page.goto('https://app.example.com/login')
# Fill credentials page.fill('input[name="password"]', 'password123') page.click('button[type="submit"]')
# Wait for dashboard page.wait_for_url('**/dashboard')
# Return confirmation return {'status': 'logged_in', 'session_active': True}
# Second call: Navigate to settings (still logged in!)def go_to_settings(): page.click('a[href="/settings"]') page.wait_for_selector('.settings-page')
# Extract settings data email = page.locator('#email-display').text_content()
return {'email': email, 'page': 'settings'}
# Third call: Update settingsdef update_notification_pref(enabled=True): page.click(f'input[name="notifications"][value="{str(enabled).lower()}"]') page.click('button[type="submit"]')
# Wait for success message page.wait_for_selector('.success-message')
return {'notifications_enabled': enabled, 'updated': True}I think the key advantage is that I only log in once. The browser instance stays alive, so cookies and session state persist across multiple calls. Each execution returns minimal data, not the entire page state.
Handling Dynamic Content and SPAs
When I scrape React apps, I need to wait for async data to load. Playwright makes this straightforward.
from playwright.sync_api import sync_playwrightimport json
def scrape_react_app(): with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page()
page.goto('https://spa.example.com/dashboard')
# Wait for specific network response with page.expect_response('**/api/data') as response_info: page.wait_for_selector('[data-testid="data-loaded"]')
response = response_info.value api_data = response.json()
# Or extract from DOM after React renders items = [] elements = page.locator('.data-item').all()
for el in elements: items.append({ 'id': el.get_attribute('data-id'), 'name': el.locator('.name').text_content(), 'value': el.locator('.value').text_content() })
browser.close()
return { 'total_items': len(items), 'items': items[:10], # Limit results 'api_response_size': len(api_data) }The key patterns I use:
- Wait for specific network responses with
expect_response - Extract structured arrays instead of raw HTML
- Limit return data size to avoid bloating the context
- Use Playwright’s advanced waiting strategies
Error Handling Best Practices
I learned the hard way that browser operations fail often. Network issues, timeouts, changed selectors. I wrap everything in try/except blocks.
def robust_scraper(url): try: with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page()
# Set timeout page.set_default_timeout(10000)
page.goto(url)
# Handle potential errors if page.locator('.error-page').is_visible(): return {'error': 'site_error', 'url': url}
# Extract data with fallbacks title_el = page.locator('h1') title = title_el.text_content() if title_el.is_visible() else 'N/A'
browser.close() return {'title': title, 'url': url}
except Exception as e: return {'error': str(e), 'url': url}You can see that I succeeded to handle:
- Timeout errors with
set_default_timeout - Site error pages with visibility checks
- Missing elements with fallback values
- Unexpected exceptions with try/except
Performance Optimization
When I scrape multiple URLs, I reuse browser contexts and disable unnecessary resources.
def optimized_scraper(urls): """Batch multiple URLs efficiently.""" with sync_playwright() as p: browser = p.chromium.launch(headless=True)
# Reuse context context = browser.new_context()
results = [] for url in urls: page = context.new_page()
# Disable unnecessary resources page.route('**/*.{png,jpg,jpeg}', lambda route: route.abort())
page.goto(url, wait_until='domcontentloaded') # Faster than 'load'
# Extract data title = page.locator('h1').text_content() results.append({'url': url, 'title': title})
page.close()
browser.close() return resultsThe performance improvements I get:
- Reuse browser context across multiple pages
- Block images and other heavy resources
- Use
domcontentloadedinstead of waiting for full page load - Close pages after scraping to free memory
How to Set Up OpenBrowser MCP
First, I installed OpenBrowser MCP:
# Via npmnpm install -g @openbrowser/mcp-server
# Or clone the repositorygit clone https://github.com/openbrowser/mcp-server.gitcd mcp-servernpm installThen I configured Claude Desktop to use it. The config file is at ~/Library/Application Support/Claude/claude_desktop_config.json:
{ "mcpServers": { "openbrowser": { "command": "node", "args": ["/path/to/openbrowser-mcp/dist/index.js"], "env": { "HEADLESS": "true" } } }}Now I can prompt Claude to use Python automation:
"Use OpenBrowser MCP to scrape the top 5 headlines from https://news.ycombinator.com.Return only the title and URL for each story."Claude generates Python code like this:
from playwright.sync_api import sync_playwright
with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto('https://news.ycombinator.com')
stories = [] for i in range(5): title = page.locator(f'.titleline >> nth={i}').text_content() url = page.locator(f'.titleline >> nth={i} >> a').get_attribute('href') stories.append({'title': title, 'url': url})
browser.close() return storiesClaude receives back clean JSON:
[ {"title": "Show HN: OpenBrowser MCP", "url": "https://..."}, {"title": "New Python Release", "url": "https://..."}, ...]No full HTML dump. No 50KB of boilerplate. Just the data I asked for.
Common Mistakes I Made
I made these mistakes when I started:
- Over-fetching data: I returned entire HTML when I only needed 3 values
- Not leveraging persistence: I logged in on every call instead of reusing session
- Ignoring error handling: I didn’t wrap browser operations in try/except
- Hardcoded selectors: I didn’t use Playwright’s robust selector strategies
- Missing cleanup: I didn’t close pages/contexts when done
The key insight from the Reddit thread: “Agent controls what comes back—don’t just dump page.content().” Extract specific elements and return structured data.
Summary
In this post, I showed how to use Python code execution with OpenBrowser MCP for web automation. The key point is that the “one tool + Python” pattern gives maximum flexibility while controlling what data gets returned.
Instead of being limited by pre-built tools, my AI agent can write any Python code it needs. The persistent runtime maintains browser state across executions. And I get back clean, structured data instead of bloated page dumps.
You can use this for scraping, form filling, testing, or any web automation task where you want an AI agent to control a real browser.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 OpenBrowser MCP GitHub
- 👨💻 Model Context Protocol Spec
- 👨💻 Playwright Python Documentation
- 👨💻 Reddit Discussion: OpenBrowser MCP
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments