Table of contents

How do I Use MCP Servers with Headless Browsers?

Model Context Protocol (MCP) servers can be seamlessly integrated with headless browsers like Puppeteer and Playwright to create powerful web scraping and browser automation workflows. This integration allows you to leverage MCP's structured tooling capabilities while performing complex browser-based data extraction tasks.

Understanding MCP and Headless Browser Integration

MCP servers provide a standardized protocol for tools and resources, while headless browsers enable programmatic control of web browsers without a graphical interface. Combining these technologies allows you to:

  • Automate browser interactions through MCP tools
  • Extract dynamic content from JavaScript-heavy websites
  • Handle authentication flows and session management
  • Capture screenshots and generate PDFs
  • Monitor network traffic and API calls

Setting Up MCP with Playwright

Playwright is one of the most popular headless browser frameworks, and it has native MCP server support. Here's how to set it up:

Installation

First, install the Playwright MCP server:

npm install @modelcontextprotocol/server-playwright

Configuration

Configure your MCP client (like Claude Desktop) to use the Playwright server. Add this to your MCP settings file:

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-playwright"
      ]
    }
  }
}

Basic Usage with Python

Here's how to interact with the Playwright MCP server using Python:

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def scrape_with_playwright_mcp():
    server_params = StdioServerParameters(
        command="npx",
        args=["-y", "@modelcontextprotocol/server-playwright"]
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # Navigate to a page
            await session.call_tool(
                "browser_navigate",
                arguments={"url": "https://example.com"}
            )

            # Take a snapshot
            snapshot = await session.call_tool(
                "browser_snapshot",
                arguments={}
            )

            # Click an element
            await session.call_tool(
                "browser_click",
                arguments={
                    "element": "Login button",
                    "ref": "element-ref-from-snapshot"
                }
            )

            print(snapshot)

asyncio.run(scrape_with_playwright_mcp())

JavaScript Implementation

You can also use JavaScript to interact with the Playwright MCP server:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

async function scrapeWithPlaywrightMCP() {
  const transport = new StdioClientTransport({
    command: "npx",
    args: ["-y", "@modelcontextprotocol/server-playwright"]
  });

  const client = new Client({
    name: "playwright-scraper",
    version: "1.0.0"
  }, {
    capabilities: {}
  });

  await client.connect(transport);

  // Navigate to page
  await client.callTool({
    name: "browser_navigate",
    arguments: {
      url: "https://example.com"
    }
  });

  // Get page snapshot
  const snapshot = await client.callTool({
    name: "browser_snapshot",
    arguments: {}
  });

  // Extract data
  const result = await client.callTool({
    name: "browser_evaluate",
    arguments: {
      function: "() => document.title"
    }
  });

  console.log("Page title:", result);

  await client.close();
}

scrapeWithPlaywrightMCP();

Working with Puppeteer MCP Server

While Playwright has official MCP support, you can also create custom MCP servers that wrap Puppeteer functionality for handling browser sessions and automation tasks.

Creating a Custom Puppeteer MCP Server

Here's an example of a basic Puppeteer MCP server:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import puppeteer from "puppeteer";

let browser = null;
let page = null;

const server = new Server({
  name: "puppeteer-mcp-server",
  version: "1.0.0"
}, {
  capabilities: {
    tools: {}
  }
});

// Tool: Launch browser
server.setRequestHandler("tools/call", async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case "launch_browser":
      browser = await puppeteer.launch({
        headless: true,
        args: ['--no-sandbox', '--disable-setuid-sandbox']
      });
      page = await browser.newPage();
      return {
        content: [{ type: "text", text: "Browser launched successfully" }]
      };

    case "navigate":
      await page.goto(args.url, { waitUntil: 'networkidle2' });
      return {
        content: [{ type: "text", text: `Navigated to ${args.url}` }]
      };

    case "screenshot":
      const screenshot = await page.screenshot({ encoding: 'base64' });
      return {
        content: [{ type: "text", text: screenshot }]
      };

    case "extract_text":
      const text = await page.evaluate(() => document.body.innerText);
      return {
        content: [{ type: "text", text: text }]
      };

    case "close_browser":
      await browser.close();
      return {
        content: [{ type: "text", text: "Browser closed" }]
      };

    default:
      throw new Error(`Unknown tool: ${name}`);
  }
});

// List available tools
server.setRequestHandler("tools/list", async () => {
  return {
    tools: [
      {
        name: "launch_browser",
        description: "Launch a headless browser instance",
        inputSchema: { type: "object", properties: {} }
      },
      {
        name: "navigate",
        description: "Navigate to a URL",
        inputSchema: {
          type: "object",
          properties: {
            url: { type: "string", description: "The URL to navigate to" }
          },
          required: ["url"]
        }
      },
      {
        name: "screenshot",
        description: "Take a screenshot of the current page",
        inputSchema: { type: "object", properties: {} }
      },
      {
        name: "extract_text",
        description: "Extract all text from the page",
        inputSchema: { type: "object", properties: {} }
      },
      {
        name: "close_browser",
        description: "Close the browser",
        inputSchema: { type: "object", properties: {} }
      }
    ]
  };
});

const transport = new StdioServerTransport();
await server.connect(transport);

Using the Custom Puppeteer MCP Server

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def scrape_with_custom_puppeteer():
    server_params = StdioServerParameters(
        command="node",
        args=["path/to/puppeteer-mcp-server.js"]
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # Launch browser
            await session.call_tool("launch_browser", arguments={})

            # Navigate to page
            await session.call_tool("navigate", arguments={
                "url": "https://example.com"
            })

            # Extract text
            result = await session.call_tool("extract_text", arguments={})
            print("Extracted text:", result.content[0].text)

            # Close browser
            await session.call_tool("close_browser", arguments={})

asyncio.run(scrape_with_custom_puppeteer())

Advanced Techniques

Handling Dynamic Content

When working with JavaScript-heavy sites, you'll need to wait for content to load. Here's how to implement waitFor functionality:

// In your MCP server tool handler
case "wait_for_selector":
  await page.waitForSelector(args.selector, {
    timeout: args.timeout || 30000
  });
  return {
    content: [{ type: "text", text: `Element ${args.selector} found` }]
  };

Managing Multiple Pages

For scraping multiple pages in parallel:

case "open_new_page":
  const newPage = await browser.newPage();
  const pageId = generateUniqueId();
  pages[pageId] = newPage;
  return {
    content: [{ type: "text", text: pageId }]
  };

case "switch_page":
  page = pages[args.pageId];
  return {
    content: [{ type: "text", text: "Page switched" }]
  };

Network Monitoring

Monitor network requests to capture API calls:

case "monitor_requests":
  const requests = [];
  page.on('request', request => {
    requests.push({
      url: request.url(),
      method: request.method(),
      headers: request.headers()
    });
  });
  return {
    content: [{ type: "text", text: JSON.stringify(requests) }]
  };

Best Practices

Resource Management

Always clean up browser instances to prevent memory leaks:

try:
    await session.call_tool("launch_browser", arguments={})
    # Perform scraping operations
finally:
    await session.call_tool("close_browser", arguments={})

Error Handling

Implement robust error handling in your MCP server:

try {
  await page.goto(args.url, { waitUntil: 'networkidle2', timeout: 30000 });
} catch (error) {
  return {
    content: [{ type: "text", text: `Navigation failed: ${error.message}` }],
    isError: true
  };
}

Performance Optimization

  • Use headless mode for better performance
  • Disable unnecessary features like images and CSS when only extracting text
  • Implement connection pooling for multiple scraping tasks
  • Set appropriate timeouts to prevent hanging operations
browser = await puppeteer.launch({
  headless: true,
  args: [
    '--no-sandbox',
    '--disable-setuid-sandbox',
    '--disable-dev-shm-usage',
    '--disable-images',
    '--disable-css'
  ]
});

Alternative: Using WebScraping.AI API

If setting up and maintaining MCP servers with headless browsers seems complex, consider using a managed solution like WebScraping.AI. It provides JavaScript rendering capabilities without the need to manage browser instances:

curl -X GET "https://api.webscraping.ai/html?url=https://example.com&js=true" \
  -H "api_key: YOUR_API_KEY"

This approach eliminates the need for browser management while still accessing dynamic content through handling AJAX requests and JavaScript-rendered pages.

Conclusion

Integrating MCP servers with headless browsers provides a powerful framework for building sophisticated web scraping solutions. Whether you use the official Playwright MCP server or create custom Puppeteer-based servers, this architecture allows for modular, maintainable, and scalable scraping workflows. Start with the Playwright MCP server for quick implementation, then consider building custom servers when you need specialized functionality or integration with existing systems.

Remember to always respect website terms of service, implement rate limiting, and handle errors gracefully to build reliable scraping applications.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon