
Build an MCP Server with Playwright Stealth for AI Browsing
Give AI agents reliable web access by building a Model Context Protocol (MCP) server with Playwright Stealth to render JS and avoid basic bot detection.
June 3, 2026
TL;DR
To give AI agents reliable web access, wrap Playwright with the playwright-stealth plugin inside a Python-based Model Context Protocol (MCP) server. This architecture exposes a standard browse_page tool to the LLM, renders JavaScript-heavy pages, masks default headless browser signatures, and returns clean Markdown to save tokens. When local stealth fails against advanced protection, offload the rendering to an API designed for anti-bot evasion.
The Agentic Browsing Problem
AI agents need real-time data to answer questions, analyze trends, or extract structured information. Standard HTTP libraries return raw HTML. If a target site relies on client-side frameworks to render content, an HTTP GET request returns an empty <body> and a bundle of JavaScript files. The agent sees nothing useful.
Headless browsers solve the rendering problem. Tools like Playwright and Puppeteer execute the JavaScript and build the final Document Object Model (DOM). However, running a headless browser out of the box introduces a new failure mode. Modern web applications use fingerprinting to detect automated traffic. A default Playwright instance leaks properties indicating it is a bot. The agent request gets intercepted by a CAPTCHA or a block page.
The solution requires three components. First, a standard interface for the agent to request web data. Second, a browser engine to render the page. Third, stealth modifications to bypass basic bot detection.
Understanding the Model Context Protocol
The Model Context Protocol (MCP) standardizes how AI applications connect to data sources. Instead of writing custom API integrations for every new agent framework, developers build an MCP server. The server exposes resources and tools over a JSON-RPC connection. The agent client discovers these capabilities and invokes them when needed.
For web browsing, an MCP server exposes a tool. The agent decides it needs to view a URL, calls the tool with the URL as an argument, and waits for the text response. The MCP server handles the browser lifecycle, the network requests, and the content extraction.
Setting Up the Project
We will build the MCP server using Python. You need Python 3.10 or higher. Create a new directory and install the required dependencies. We use the official mcp SDK, playwright for browsing, playwright-stealth for evasion, and markdownify to convert HTML to text.
mkdir mcp-browser
cd mcp-browser
python -m venv venv
source venv/bin/activate
pip install mcp playwright playwright-stealth markdownify
playwright install chromiumArchitecting the MCP Server
An MCP server handles initialization, discovers tools, and executes them. The mcp Python package provides a FastMCP class that simplifies routing and tool registration.
We define a server instance and register a browse function. The function takes a URL string and returns the page content. The type hints and docstrings are critical. The agent client reads the docstring to understand when and how to use the tool.
from mcp.server.fastmcp import FastMCP
from playwright.async_api import async_playwright
from playwright_stealth import stealth_async
from markdownify import markdownify
# Initialize the MCP server
mcp = FastMCP("StealthBrowser")
@mcp.tool()
async def browse(url: str) -> str:
"""
Fetch and render a webpage.
Use this tool when you need to read content from a specific URL.
Returns the page content in Markdown format.
"""
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context()
page = await context.new_page()
# We will add stealth here shortly
await page.goto(url, wait_until="networkidle")
html_content = await page.content()
await browser.close()
# Convert to token-efficient Markdown
text_content = markdownify(html_content, strip=['script', 'style'])
return text_content
if __name__ == "__main__":
mcp.run_stdio_async()This basic implementation works for simple sites. The wait_until="networkidle" directive ensures the browser waits for XHR requests to complete before dumping the HTML. We pass the raw HTML through markdownify to strip out navigation menus, scripts, and CSS. This reduces the token load on the LLM, making the response cheaper and faster to process.
Applying Playwright Stealth
The basic server fails on domains with standard bot protection. Security scripts look for the webdriver property on the navigator object. They check for the presence of standard browser plugins. They inspect the user agent string.
The playwright-stealth package injects scripts into the page before the main content loads. These scripts overwrite the navigator properties, mock the plugins array, and patch the window.chrome object to mimic a regular user session.
Let us modify the browse tool to implement stealth. We initialize the stealth modifications immediately after creating the page object.
@mcp.tool()
async def browse(url: str) -> str:
"""
Fetch and render a webpage.
Returns the page content in Markdown format.
"""
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context()
page = await context.new_page()
# Apply stealth patches before navigation
await stealth_async(page)
try:
await page.goto(url, wait_until="networkidle", timeout=30000)
html_content = await page.content()
except Exception as e:
await browser.close()
return f"Error loading page: {str(e)}"
await browser.close()
return markdownify(html_content, strip=['script', 'style'])By calling await stealth_async(page), the plugin overrides the default headless identifiers. The browser now reports itself correctly, masks the automation flags, and has a higher probability of loading the target content without triggering a CAPTCHA.
Context Management and Performance
Launching a new browser instance for every request adds significant latency. A cold start takes several seconds. For a production MCP server, you should maintain a persistent browser instance and open new contexts for each request.
A browser context is an isolated session. It has its own cookies, cache, and local storage. By reusing the browser process and creating fresh contexts, you reduce the tool execution time by up to 60%.
from mcp.server.fastmcp import FastMCP
from playwright.async_api import async_playwright
from playwright_stealth import stealth_async
from markdownify import markdownify
import asyncio
mcp = FastMCP("StealthBrowser")
browser_instance = None
@mcp.lifespan()
async def manage_browser():
global browser_instance
playwright = await async_playwright().start()
browser_instance = await playwright.chromium.launch(headless=True)
yield
await browser_instance.close()
await playwright.stop()
@mcp.tool()
async def browse(url: str) -> str:
global browser_instance
context = await browser_instance.new_context()
page = await context.new_page()
await stealth_async(page)
try:
await page.goto(url, wait_until="networkidle", timeout=30000)
html_content = await page.content()
except Exception as e:
await context.close()
return f"Error loading page: {str(e)}"
await context.close()
return markdownify(html_content, strip=['script', 'style'])The @mcp.lifespan() decorator handles the startup and shutdown sequence. The browser process runs continuously while the MCP server is active. The agent can issue multiple sequential requests with minimal overhead.
Limitations of Local Stealth
Playwright Stealth handles client-side javascript checks. It masks the webdriver flag and normalizes browser fingerprints. However, modern bot detection relies on more than just JavaScript inspection.
Advanced protection systems analyze network-level signals. They check the TLS fingerprint of the request. Python's underlying network stack and standard Chromium builds have distinct TLS signatures compared to consumer browsers. Furthermore, the IP address origin matters heavily. Datacenter IP addresses from cloud providers get flagged automatically, regardless of how stealthy the browser environment appears.
When dealing with strict environments, the local Playwright approach requires constant maintenance. You find yourself managing proxy rotation pools, writing custom CAPTCHA solving logic, and keeping TLS signatures updated. This shifts your engineering focus away from building agent capabilities and towards fighting anti-bot systems.
Scaling with Infrastructure APIs
Instead of managing proxies and patching headless browsers locally, you can modify the MCP server to offload the heavy lifting. Using an API designed for anti-bot handling abstracts the browser cluster, IP rotation, and evasion logic into a single endpoint.
The agent still calls the browse_page tool, but the backend implementation changes. Instead of launching Playwright, the MCP server makes an HTTP request to the extraction API.
Here is how you implement the tool using the Python SDK.
import alterlab
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("ReliableBrowser")
client = alterlab.Client("YOUR_API_KEY")
@mcp.tool()
def browse(url: str) -> str:
"""
Fetch page content reliably. Use this tool to read web pages.
"""
try:
response = client.scrape(
url=url,
render_js=True,
formats=["markdown"]
)
return response.markdown
except Exception as e:
return f"Scraping failed: {str(e)}"This approach eliminates the local browser dependency. The formats=["markdown"] parameter handles the HTML-to-text conversion server-side, returning clean data ready for the LLM.
For environments without Python dependencies, you can achieve the same result using a standard cURL request to the REST API. The concept remains identical. Send the target URL, configure the rendering parameters, and receive the processed payload.
curl -X POST https://api.alterlab.io/v1/scrape \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/target-data",
"render_js": true,
"formats": ["markdown"]
}'You can test the extraction output directly in your browser. This interactive widget demonstrates the payload structure returned to the MCP server.
Test URL rendering with automatic evasion
To start integrating this workflow into your local agents, view the quickstart guide to generate an API key and configure your environment variables.
Security Considerations for MCP
Exposing web browsing tools to autonomous agents introduces security risks. An LLM acting on user prompts might attempt to access internal network resources or local development servers. If your MCP server runs on a machine with access to internal infrastructure, the agent could extract sensitive data.
You must implement network restrictions within the browse tool. Validate the URL before initializing the browser or making the API call. Ensure the domain resolves to public IP space and block local addresses like localhost, 127.0.0.1, or 192.168.x.x.
Additionally, enforce timeouts. Agents can get stuck waiting for pages that never trigger the networkidle event due to continuous background polling by the target site. Set strict execution limits on the tool to ensure the agent receives an error and can attempt an alternative strategy.
Takeaways
Building a Model Context Protocol server transforms a static LLM into an active web researcher. By combining MCP with Playwright Stealth, you give your agents the ability to read dynamic content while avoiding basic detection mechanisms.
- Standard HTTP clients cannot render JavaScript applications.
- Default headless browsers leak automation signatures and get blocked.
- Playwright Stealth masks client-side properties to improve access rates.
- Maintaining persistent browser contexts reduces tool execution latency.
- For strict targets, offload rendering and IP rotation to a dedicated extraction API.
Implementing these patterns ensures your agents spend their time analyzing data, not timing out on block pages.
Was this article helpful?
Frequently Asked Questions
Related Articles
Popular Posts
Recommended
Newsletter
Scraping insights and API tips. No spam.
Recommended Reading

How to Scrape Amazon in 2026: Engineering Guide

How to Scrape AliExpress: Complete Guide for 2026

Why Your Headless Browser Gets Detected (and How to Fix It)

How to Scrape Indeed: Complete Guide for 2026

How to Scrape Twitter/X Data: Complete Guide for 2026
Stay in the Loop
Get scraping insights, API tips, and platform updates. No spam — we only send when we have something worth reading.


