Build an MCP Server with Playwright Stealth for AI Browsing
Tutorials

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.

8 min read
9 views

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.

Bash
mkdir mcp-browser
cd mcp-browser
python -m venv venv
source venv/bin/activate
pip install mcp playwright playwright-stealth markdownify
playwright install chromium

Architecting 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.

Python
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.

Python
@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%.

Python
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.

Python
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.

Bash
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.

Try it yourself

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.

Share

Was this article helpful?

Frequently Asked Questions

MCP is an open standard that allows AI agents to securely connect to local and remote data sources using a client-server architecture. It provides a uniform way for models to use tools and access resources.
Standard headless browsers leak fingerprints that trigger bot detection on modern web pages. Playwright Stealth masks these signatures, allowing agents to access dynamically rendered content without being blocked.
After rendering the page with Playwright, extract the DOM and convert it to clean Markdown. This reduces token usage and improves the model's ability to extract structured data.