Skip to main content
Glama

Playwright MCP Server

A production-ready Model Context Protocol (MCP) server that exposes full browser automation over HTTP — built with the MCP Python SDK v2 (MCPServer) and Playwright, deployable to Azure Container Apps. Supports multiple concurrent clients via isolated sessions — all output returned inline (base64 for images), nothing written to disk.

Python MCP SDK Playwright Azure

Control a real Chromium browser through simple HTTP calls. No Selenium, no WebDriver setup, no manual MCP protocol boilerplate. Each client gets an isolated browser session (separate cookies, localStorage, history) so multiple agents can operate in parallel. All output is returned inline — screenshots as base64, DOM as HTML — nothing is written to disk. Requests are authenticated via an x-api-key header, with the secret loaded from a .env file.

When to Use This

Use Case

Example

AI agent browser control

Let an LLM agent navigate, click, and scrape web pages via MCP tool calls

End-to-end test automation

Drive a real Chromium browser over HTTP from any language or test runner

Web scraping pipelines

Extract DOM snapshots, evaluate JS, and capture screenshots programmatically

RPA (Robotic Process Automation)

Automate repetitive web workflows — form filling, file uploads, multi-tab flows

Automated screenshots / visual monitoring

Capture full-page screenshots of any URL on demand

Remote browser execution

Run browser automation on a server or in Azure without a local display

When NOT to Use This

  • You only need static HTML parsing — use requests + BeautifulSoup instead

  • You need parallel browsers at scale — consider Playwright's own grid or Playwright Test

  • You need a full GUI test framework with assertions and reports — use Playwright Test or Pytest-Playwright directly

Architecture

Project Files

File

Purpose

server.py

Main MCP server — registers all browser tools, applies API-key auth middleware, and starts MCPServer via uvicorn

validate_server.py

Pre-flight checker — verifies all imports and runtime dependencies

requirements.txt

Python dependency list

.env

Environment variables (API key, etc.) — not committed to git

How It Works

  • server.py creates an MCPServer instance and builds the ASGI app with mcp.streamable_http_app(stateless_http=True)

  • An APIKeyMiddleware (Starlette) is added to the ASGI app — every request must include a valid x-api-key header

  • The API key is loaded from a .env file via python-dotenv at startup (MCP_API_KEY variable)

  • Session-based: each client calls session_create to get an isolated browser context (separate cookies, localStorage, page). Multiple sessions run in parallel on a shared Chromium instance

  • Playwright is lazily initialized — the Chromium browser only starts on the first session creation

  • No disk I/O — screenshots are returned as base64, DOM snapshots as HTML strings. Nothing is written to the filesystem

  • Idle sessions are automatically evicted after SESSION_IDLE_TIMEOUT_S (default 10 minutes). The server enforces MAX_SESSIONS (default 10)

  • A health_check tool acts as a lightweight health probe — it responds instantly without starting the browser

  • The server binds to 0.0.0.0 and reads the PORT environment variable (default 8000)

Architecture Diagram

flowchart LR
    ClientA[Client A]
    ClientB[Client B]
    subgraph Server[Playwright MCP Server]
      direction TB
      Auth[APIKeyMiddleware]
      MCPServer[MCPServer]
      Sessions[Session Manager]
      Browser[Shared Chromium Instance]
      CtxA[Context A / Page A]
      CtxB[Context B / Page B]
    end
    ClientA -->|"HTTP + x-api-key"| Auth
    ClientB -->|"HTTP + x-api-key"| Auth
    Auth -->|valid key| MCPServer
    Auth -->|invalid key| ClientA
    MCPServer -->|calls tool| Sessions
    Sessions -->|session A| CtxA
    Sessions -->|session B| CtxB
    CtxA -->|isolated browsing| Browser
    CtxB -->|isolated browsing| Browser
    Sessions -->|"JSON + base64"| MCPServer
    MCPServer -->|HTTP response| ClientA
    MCPServer -->|HTTP response| ClientB

Step-by-step:

  1. Client sends an HTTP POST to https://<host>/mcp with an x-api-key header

  2. APIKeyMiddleware validates the key — returns 401 if invalid or missing

  3. Client calls session_create to get an isolated session_id

  4. All subsequent tool calls include session_id to target the correct browser context

  5. The shared Chromium instance is lazily started on the first session creation

  6. Results (HTML, base64 images, JSON) are returned inline — nothing written to disk

  7. Client calls session_close when done; idle sessions are auto-evicted

Features

Multi-client sessions

Each client gets an isolated browser context (cookies, storage, page) via session_create. Multiple agents work in parallel

No disk I/O

Screenshots returned as base64, DOM as HTML. Nothing written to the filesystem — safe for serverless/container deployments

API-key authentication

All requests require a valid x-api-key header — key loaded from .env, compared with hmac.compare_digest

Auto-cleanup

Idle sessions evicted after configurable timeout; max session limit enforced with LRU eviction

Lazy-loaded browser

Shared Chromium instance starts on first session creation — zero startup overhead

Full browser automation

Navigate, click, type, screenshot, evaluate JS, DOM snapshot, resize, and key press

Azure Container Apps ready

Built-in health_check tool satisfies health probes; deploy with scripts/deploy-aca.ps1

Async / await

Every tool is a native Python async function — no blocking calls

Streamable HTTP transport

MCPServer uses streamable-http for real-time MCP communication

Distributed tracing (OTel)

Built-in OpenTelemetry instrumentation with W3C traceparent propagation. Traces flow from calling services through MCP tool calls to Splunk APM / Jaeger / any OTLP backend

Tools Available

All browser tools accept an optional session_id parameter. If omitted, a new session is auto-created.

Session Management

Tool

Description

Input

health_check()

Server health, active session count

None

session_create()

Create an isolated browser session

None

session_close()

Close a session and free resources

session_id

session_list()

List all active sessions with idle times

None

Browser Automation

Tool

Description

Input

browser_navigate()

Navigate to a URL (http, https, or data: URI)

session_id?, url

browser_click()

Click an element by selector

session_id?, ref

browser_type()

Type text into an input field

session_id?, ref, text

browser_press_key()

Press a keyboard key

session_id?, key

browser_take_screenshot()

Capture page as base64 PNG

session_id?, fullPage?

browser_pdf_save()

Generate a PDF of the current page

session_id?, format?, landscape?, print_background?, scale?, upload_to_storage?, html?

browser_snapshot()

Get full HTML DOM content

session_id?

browser_evaluate()

Execute JavaScript in page context

session_id?, function

browser_resize()

Resize the browser viewport

session_id?, width, height

Prerequisites

  • Python 3.8+

  • pip or conda

Installation

  1. Clone or download this project

  2. Create a virtual environment

    python -m venv .venv
  3. Activate the virtual environment

    • Windows:

      .venv\Scripts\activate
    • Linux/macOS:

      source .venv/bin/activate
  4. Install dependencies

    pip install -r requirements.txt
  5. Configure environment variables

    Copy the template and fill in your API key:

    # Linux/macOS
    cp .env.example .env
    
    # Windows
    copy .env.example .env

    Then edit .env and set at minimum:

    MCP_API_KEY=your-secure-random-key-here

    See .env.example for all available settings (timeouts, session limits, Azure config, etc.).

    ⚠️ The .env file is git-ignored by default. Never commit your API key to source control.

Running Locally

python server.py

The server will start on http://0.0.0.0:8000 by default. All requests must include the x-api-key header matching the MCP_API_KEY value from your .env file.

Custom Port

Set the PORT environment variable to use a different port:

# Windows
set PORT=3000
python server.py

# Linux/macOS
export PORT=3000
python server.py

Connect from Zoo Code

To use this MCP server from Zoo Code, add the following to your MCP settings file (mcp_settings.json):

{
  "mcpServers": {
    "playwright_local": {
      "type": "streamable-http",
      "url": "http://localhost:8100/mcp",
      "headers": {
        "x-api-key": "YOUR_KEY_HERE"
      },
      "alwaysAllow": []
    }
  }
}

Replace YOUR_KEY_HERE with the MCP_API_KEY value from your .env file, and adjust the port if you changed PORT in .env.

Tip: Make sure the Docker container is running before connecting. Use .\scripts\start-docker.ps1 or start it manually with docker run.

Quick Usage Example

With the server running, call any tool via the /mcp endpoint. You must include the x-api-key header with every request.

1. Create a session

curl -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -H "x-api-key: your-secure-random-key-here" \
  -d '{
    "method": "tools/call",
    "params": { "name": "session_create", "arguments": {} }
  }'

Response:

{ "session_id": "a1b2c3d4-...", "status": "created" }

2. Navigate (pass the session_id)

curl -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -H "x-api-key: your-secure-random-key-here" \
  -d '{
    "method": "tools/call",
    "params": {
      "name": "browser_navigate",
      "arguments": { "session_id": "a1b2c3d4-...", "url": "https://example.com" }
    }
  }'

3. Take a screenshot (returned as base64)

curl -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -H "x-api-key: your-secure-random-key-here" \
  -d '{
    "method": "tools/call",
    "params": {
      "name": "browser_take_screenshot",
      "arguments": { "session_id": "a1b2c3d4-...", "fullPage": true }
    }
  }'

Response:

{ "base64_png": "iVBORw0KGgoAAAANSUhEUgAA...", "format": "png" }

4. Close the session when done

curl -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -H "x-api-key: your-secure-random-key-here" \
  -d '{
    "method": "tools/call",
    "params": {
      "name": "session_close",
      "arguments": { "session_id": "a1b2c3d4-..." }
    }
  }'

Tip: If you omit session_id from any browser tool, a new session is auto-created and its ID is returned in the response.

If the API key is missing or incorrect, you will receive:

{ "error": "Unauthorized. Provide a valid x-api-key header." }

with HTTP status 401.

Test With Postman

You can also test MCP functionality using Postman and the running http://localhost:8000/mcp endpoint.

  1. Open Postman.

  2. Create a new MCP request or a new HTTP POST request if your version does not expose MCP request types.

  3. Set the request URL to:

    http://localhost:8000/mcp
  4. Add the authentication header:

    • Go to the Headers tab.

    • Add a header with key x-api-key and value set to your MCP_API_KEY from the .env file.

  5. Create a session first, then use the returned session_id in subsequent calls:

    {
      "method": "tools/call",
      "params": {
        "name": "session_create",
        "arguments": {}
      }
    }
  6. Use the session_id from the response to call browser tools:

    {
      "method": "tools/call",
      "params": {
        "name": "browser_navigate",
        "arguments": {
          "session_id": "<session_id from step 5>",
          "url": "https://example.com"
        }
      }
    }
  7. Click Send or Run.

  8. Review the response panel to confirm the tool execution and any returned status.

Deployment to Azure

The fastest path to a public HTTPS endpoint with Azure-managed TLS certificates. A single PowerShell script handles everything.

Prerequisites:

  • Azure CLI installed and logged in (az login)

  • Container Apps CLI extension: az extension add --name containerapp --upgrade

  • Python 3.8+

Steps:

  1. Configure your .env file with Azure settings:

    AZURE_SUBSCRIPTION_ID=<your-subscription-id>
    AZURE_RESOURCE_GROUP=mcp-playwright-rg
    AZURE_LOCATION=westeurope
    AZURE_APP_NAME=mcp-playwright
    MCP_API_KEY=                              # leave empty to auto-generate
  2. Run the deploy script:

    .\scripts\deploy-aca.ps1
  3. The script will:

    • Create a resource group

    • Build your Docker image and push it to a managed Azure Container Registry

    • Create an Azure Container App with public HTTPS ingress

    • Inject MCP_API_KEY, HEADLESS, and PORT as environment variables

    • Run a smoke test against the health_check tool

    • Print the full HTTPS endpoint and API key

  4. Test the endpoint:

    curl -X POST https://<your-app>.<region>.azurecontainerapps.io/mcp \
      -H "Content-Type: application/json" \
      -H "x-api-key: <your-key>" \
      -d '{"method":"tools/call","params":{"name":"browser_navigate","arguments":{"url":"https://example.com"}}}'
  5. Tear down when finished:

    .\scripts\teardown-aca.ps1

See plans/azure-quick-test-aca.md for the full architecture and alternative deployment options.

Alternative: Azure App Service for Containers

  1. Create an App Service with a custom container

    az appservice plan create --name <plan-name> --resource-group <rg-name> --sku B2 --is-linux
    az webapp create --resource-group <rg-name> --plan <plan-name> --name <app-name> --deployment-container-image-name <your-acr>.azurecr.io/playwright-mcp-server:latest
  2. Configure environment (including the API key)

    az webapp config appsettings set --resource-group <rg-name> --name <app-name> \
      --settings PORT=8000 HEADLESS=true MCP_API_KEY=your-secure-random-key-here
  3. Configure the health probe (Azure Portal → App Service → Health Check)

    • Set path to /mcp

Local Docker

Quick start (recommended): Use the provided PowerShell script to build and run on port 3010 (avoids conflicts with the default 8000):

.\scripts\start-docker.ps1

This will:

  • Build the Docker image from the project Dockerfile

  • Stop and remove any existing playwright-mcp container

  • Start a new container mapping host port 3010 → container port 8000

  • Read MCP_API_KEY from .env (or auto-generate one)

  • Print the endpoint URL, API key, and test commands

The MCP endpoint will be available at http://localhost:3010/mcp.

Options:

# Use a custom host port
.\scripts\start-docker.ps1 -Port 4000

# Force a no-cache rebuild
.\scripts\start-docker.ps1 -Rebuild

# Both
.\scripts\start-docker.ps1 -Port 4000 -Rebuild

Manual build and run (if you prefer not to use the script):

docker build -t playwright-mcp-server .
docker run -p 3010:8000 --env-file .env playwright-mcp-server

Note: The container listens on port 8000 internally. The -p 3010:8000 flag maps host port 3010 to the container's port 8000, avoiding conflicts if port 8000 is already in use on your machine.

Environment Variables

Variable

Default

Description

MCP_API_KEY

(none — required)

Secret key for x-api-key header authentication. The server will return 500 on all requests if this is not set.

PORT

8000

Port the server listens on

HEADLESS

true

Set to false to show the browser window (local dev only)

NAVIGATION_TIMEOUT_MS

30000

Maximum time (ms) to wait for page navigation (e.g. page.goto). Increase for slow sites.

ACTION_TIMEOUT_MS

10000

Maximum time (ms) to wait for element actions (click, fill, etc.).

SESSION_IDLE_TIMEOUT_S

600

Seconds a session can sit idle before automatic eviction (default 10 minutes).

MAX_SESSIONS

10

Maximum number of concurrent browser sessions. When the limit is reached, the oldest idle session is evicted to make room.

UVICORN_WORKERS

1

Number of uvicorn workers. Keep at 1 (Playwright state is in-process). Scale horizontally with container replicas.

ENABLE_HTTPS

false

Set to true to serve over HTTPS using a self-signed certificate (baked into the Docker image).

SSL_CERTFILE

/app/certs/server.crt

Path to TLS certificate file. Only used when ENABLE_HTTPS=true.

SSL_KEYFILE

/app/certs/server.key

Path to TLS private key file. Only used when ENABLE_HTTPS=true.

DOCKER_HOST_PORT

3010

Host port used by scripts/start-docker.ps1 for the Docker port mapping.

OTEL_EXPORTER_OTLP_ENDPOINT

(empty)

OTLP/HTTP endpoint for the OTel Collector (e.g. http://localhost:4318). When set, traces are exported via OTLP.

OTEL_SERVICE_NAME

playwright-mcp

Service name shown in trace backends (Splunk APM, Jaeger, etc.).

OTEL_RESOURCE_ATTRIBUTES

(empty)

Extra resource attributes as key=value,key=value (e.g. deployment.environment=dev).

All variables can be set in the .env file (loaded automatically via python-dotenv) or passed directly as environment variables.

Scalability

This server runs one shared Chromium instance per process with multiple isolated sessions. To scale:

Strategy

Details

Session isolation

Each client gets its own BrowserContext + Page — separate cookies, localStorage, and browsing history. Up to MAX_SESSIONS (default 10) run concurrently.

Auto-eviction

Idle sessions are evicted after SESSION_IDLE_TIMEOUT_S. When MAX_SESSIONS is reached, the oldest idle session is evicted to make room for new clients.

Horizontal replicas

Deploy multiple container instances behind a load balancer (Azure Container Apps, Kubernetes, App Service scale-out). Each replica manages its own browser and session pool.

Keep workers = 1

UVICORN_WORKERS must stay at 1 because Playwright state (browser, sessions) is in-process and not shared across workers.

Graceful shutdown

The server closes all sessions and the browser cleanly on SIGTERM (Docker stop, Azure restart) via atexit + signal handlers — preventing orphaned Chromium processes.

Timeouts

All browser actions enforce configurable timeouts (NAVIGATION_TIMEOUT_MS, ACTION_TIMEOUT_MS) so a hung page doesn't block the server indefinitely.

No disk I/O

All output (screenshots, DOM) is returned inline — no filesystem writes mean the server is safe for read-only container filesystems and serverless deployments.

Docker layer caching

The Dockerfile splits pip install and Playwright browser download into separate layers — code-only changes rebuild in seconds, not minutes.

Testing

Validate Dependencies

python validate_server.py

Run the Test Suite

python test_fastmcp_simple.py

MCP Inspector is the official interactive tool for exploring and testing MCP servers. It gives you a visual UI to browse all registered tools, call them with custom inputs, and inspect responses — no Postman or curl needed.

Prerequisites: Node.js installed

  1. Start the server:

    python server.py
  2. In a new terminal, launch MCP Inspector:

    npx @modelcontextprotocol/inspector
  3. Open the Inspector UI (it will print a local URL, typically http://localhost:5173)

  4. Connect to your running server:

    • Transport: SSE or Streamable HTTP

    • URL: http://localhost:8000/mcp

  5. You will see all registered tools (session_create, session_close, browser_navigate, browser_click, browser_take_screenshot, etc.) listed in the left panel. Click any tool, fill in the inputs, and hit Run Tool to test it live.

MCP Inspector is the fastest way to verify your server is working correctly before deploying to Azure.

Project Structure

.
├── server.py                       # Main MCP server (API-key auth, browser tools, uvicorn)
├── .env                            # Environment variables — runtime + Azure config (git-ignored)
├── .env.example                    # Template with all available settings — copy to .env
├── requirements.txt                # Python dependencies
├── Dockerfile                      # Container image definition (non-root, layered)
├── .dockerignore                   # Files excluded from Docker build context
├── .gitignore                      # Files excluded from version control
├── test_fastmcp_simple.py          # Test suite
├── validate_server.py              # Dependency validation
├── scripts/
│   ├── start-docker.ps1            # Build & run locally in Docker
│   ├── deploy-aca.ps1              # One-command Azure Container Apps deployment
│   ├── teardown-aca.ps1            # Delete all Azure resources
│   └── generate_cert.py            # Generate self-signed TLS cert (used during Docker build)
├── plans/
│   └── azure-quick-test-aca.md     # Architecture plan for Azure deployment
├── README.md                       # This file
└── .venv/                          # Virtual environment (auto-generated)

Troubleshooting

All requests return 401 Unauthorized:

  • Ensure you are sending the x-api-key header with the correct value

  • Check that MCP_API_KEY is set in your .env file (or environment)

All requests return 500 — "Server API key not configured":

  • The MCP_API_KEY environment variable is missing or empty

  • Create or edit the .env file and add MCP_API_KEY=your-key-here

Browser won't initialize:

  • Ensure Playwright chromium is installed: playwright install chromium

  • Check system dependencies are installed (Linux/macOS)

Port already in use:

  • Change the PORT environment variable to an available port

Timeout errors:

  • Increase NAVIGATION_TIMEOUT_MS or ACTION_TIMEOUT_MS in .env

  • Check network connectivity to target URLs

Session not found / evicted:

  • Sessions are automatically evicted after SESSION_IDLE_TIMEOUT_S (default 10 minutes) of inactivity

  • If a session's page crashes, the server detects it and removes the session on the next access

  • Simply call session_create to get a fresh session and retry your workflow

JavaScript evaluation fails:

  • The function must be a valid JS expression returning a serializable value

  • Example: "() => document.title" not "document.title"

Security

This project includes several layers of security hardening:

Control

Details

API-key authentication

Every request must carry a valid x-api-key header. The key is compared using hmac.compare_digest to prevent timing-attack side-channels.

Cryptographic session IDs

Session IDs are generated with secrets.token_hex(16) (128-bit), making brute-force guessing infeasible even with a valid API key.

URL scheme allowlist

browser_navigate only accepts http://, https://, and data: URLs — file://, javascript:, and other dangerous schemes are rejected. data: URIs are permitted to support inline HTML rendering for PDF generation workflows.

Session isolation

Each client operates in its own BrowserContext — cookies, localStorage, and history are fully isolated between sessions.

Dead session detection

Every tool call verifies the page is still alive before use. The background cleanup also reaps crashed/dead sessions every 60 seconds.

Leak-proof creation

If page creation fails during session_create, the browser context is closed immediately — no orphaned resources.

TOCTOU-safe session limits

The MAX_SESSIONS check and session insertion happen atomically under a single lock acquisition — no race window for exceeding the limit.

Input length limits

All string fields enforce max_length via Pydantic to prevent memory exhaustion from oversized payloads.

Viewport bounds

browser_resize limits dimensions to 7680 × 4320 (8K) to prevent unreasonable resource allocation.

Race-condition guard

Browser initialisation uses an asyncio.Lock with double-checked locking to prevent duplicate Chromium launches.

Non-root Docker container

The Dockerfile creates and switches to a dedicated appuser (UID 1001) so Chromium never runs as root.

Minimal error disclosure

Server misconfiguration errors return a generic message — no internal paths or config details are leaked.

Known trade-offs

Item

Notes

--no-sandbox Chromium flag

Required inside Docker (no kernel namespaces). Mitigated by running as non-root in an isolated container.

browser_evaluate accepts arbitrary JS

This is intentional — the tool is designed for trusted callers behind the API key. Do not expose this server to untrusted users.

No rate limiting

Consider adding rate-limiting middleware (e.g. slowapi) for public-facing deployments.

No HTTPS termination by default

Set ENABLE_HTTPS=true for self-signed TLS, or use a reverse proxy (nginx, Azure Front Door, App Service TLS) for production.

Generating a strong API key

python -c "import secrets; print(secrets.token_urlsafe(32))"

Paste the output into your .env file as MCP_API_KEY=<value>.

Below are PowerShell-first commands to build and run this project on Docker Desktop and ensure other containers in the same environment can access it. Key implementation details in code: binds to 0.0.0.0 via uvicorn.run(), reads the port from PORT, requires an API key via APIKeyMiddleware reading MCP_API_KEY, and exposes 8000 in the image (EXPOSE 8000).

  1. Build the image

  • From the project root (where Dockerfile and server.py live), build a local tag: docker build -t playwright-mcp:local .

  1. Create a user-defined bridge network (enables container-to-container DNS)

  • This lets other containers reach this service by its container name instead of an IP. docker network create mcp-net

  1. Provide the API key environment

  • The server rejects requests without a valid x-api-key header enforced by APIKeyMiddleware. Create a .env at the repo root: Set-Content -Path .env -Value "MCP_API_KEY=your-secure-random-key-here"

  • Optional: add a custom port (defaults to 8000) and headless setting: Add-Content -Path .env -Value "PORT=8000" Add-Content -Path .env -Value "HEADLESS=true"

  1. Run the container (host-accessible AND accessible from other containers)

  • Maps host port 8000 to container port 8000, attaches to the shared network, and loads .env. docker run --name mcp
    --detach
    --restart unless-stopped
    --network mcp-net
    --env-file .env
    -p 8000:8000
    playwright-mcp:local

  1. Validate from the host (PowerShell)

  • Build request body to call the health_check() tool over HTTP: $headers = @{ 'Content-Type' = 'application/json'; 'x-api-key' = 'your-secure-random-key-here' } $body = @{ method = 'tools/call'; params = @{ name = 'health_check'; arguments = @{} } } | ConvertTo-Json -Depth 5 Invoke-RestMethod -Method POST -Uri 'http://localhost:8000/mcp' -Headers $headers -Body $body

  • Expected response: { status = 'healthy', ... } (browser starts only when a session is created).

  1. Validate from another container on the same network

  • Other containers should talk to this service via its container name mcp on port 8000 (not localhost). docker run --rm --network mcp-net curlimages/curl:8.6.0
    -s -H "Content-Type: application/json"
    -H "x-api-key: your-secure-random-key-here"
    -d "{"method":"tools/call","params":{"name":"health_check","arguments":{}}}"
    http://mcp:8000/mcp

Variant: inter-container access only (no host port published)

  • Omit -p to keep it internal; other containers in mcp-net still reach http://mcp:8000. docker run --name mcp
    --detach --restart unless-stopped
    --network mcp-net
    --env-file .env
    playwright-mcp:local

Changing the port (if needed)

  • If you set PORT in .env (e.g., 8931), the container listens on that port per PORT. Publish and target the same container port: Add-Content -Path .env -Value "PORT=8931" docker run --name mcp --detach --network mcp-net --env-file .env -p 8931:8931 playwright-mcp:local

  • Other containers in mcp-net then use http://mcp:8931/mcp.

Operational diagnostics and logs

  • Follow logs to verify startup; you should see the line emitted at app start "Starting Playwright MCP server on 0.0.0.0:{port}": docker logs -f mcp

  • Confirm the container is on the shared network and has a stable name: docker inspect mcp --format '{{json .NetworkSettings.Networks}}' | Out-String docker network inspect mcp-net | Out-String

  • If requests fail, ensure the x-api-key header matches the .env value and you are posting to /mcp.

Cleanup

  • Stop and remove the container, remove the network, optionally remove the image: docker rm -f mcp docker network rm mcp-net docker rmi playwright-mcp:local

Common pitfalls (and quick checks)

  • Missing MCP_API_KEY ⇒ All requests get 500 via APIKeyMiddleware. Check .env and -Headers 'x-api-key'.

  • Using localhost from another container ⇒ Use http://mcp:8000 (container name on user-defined network), not localhost.

  • Wrong port mapping when changing PORT ⇒ If PORT!=8000 in the container, publish -p hostPort:containerPort accordingly.

  • Docker Desktop set to Windows containers ⇒ This image is Linux-based (Dockerfile); switch to Linux containers.

  • Not on a user-defined network ⇒ Create mcp-net and attach both the service and clients for DNS-based service discovery.

What this enables

References in the code for clarity

This set of commands deploys the service to Docker Desktop and configures networking so other containers in the same environment can reliably reach it by container name on the shared bridge network.

OpenTelemetry / Distributed Tracing

The server includes built-in OpenTelemetry instrumentation for distributed tracing. When a calling service (e.g. a Node.js RAG API) sends a W3C traceparent header with its MCP tool calls, the Playwright server automatically continues the trace -- so you see the full request flow across services in Splunk APM, Jaeger, or any OTLP-compatible backend.

How it works

  1. ASGI middleware (OpenTelemetryMiddleware) extracts traceparent and tracestate from incoming HTTP headers and creates a server span linked to the caller's trace.

  2. Tool-level spans -- each MCP tool function is wrapped with a @traced_tool decorator that creates a child span named mcp.tool.<function_name> (e.g. mcp.tool.browser_navigate).

  3. OTLP export -- when OTEL_EXPORTER_OTLP_ENDPOINT is set, spans are exported via OTLP/HTTP to the configured OTel Collector.

Enabling tracing

Add these to your .env:

OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_SERVICE_NAME=playwright-mcp
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=dev

Startup diagnostics

On container start, the server prints a startup banner that shows whether OTel tracing is enabled and whether the collector is reachable. This makes it easy to verify your configuration without sending test requests.

OTel disabled (no endpoint configured):

==============================================================
  Playwright MCP Server — Startup Summary
==============================================================
  Listen address : http://0.0.0.0:8000
  ...
--------------------------------------------------------------
  OpenTelemetry (Distributed Tracing)
--------------------------------------------------------------
  Status         : DISABLED
  Detail         : OTEL_EXPORTER_OTLP_ENDPOINT is not set.
  To enable      : Set OTEL_EXPORTER_OTLP_ENDPOINT in your
                   .env file (e.g. http://localhost:4318).
==============================================================

OTel enabled and connected:

--------------------------------------------------------------
  OpenTelemetry (Distributed Tracing)
--------------------------------------------------------------
  Status         : ✅ ENABLED & CONNECTED
  Endpoint       : http://otel-collector:4318
  Service name   : playwright-mcp
  Traces will be exported via OTLP/HTTP.
==============================================================

OTel enabled but collector unreachable (with debug hints):

--------------------------------------------------------------
  OpenTelemetry (Distributed Tracing)
--------------------------------------------------------------
  Status         : ⚠️  ENABLED but COLLECTOR UNREACHABLE
  Endpoint       : http://otel-collector:4318
  Error          : Connection failed: [Errno 111] Connection refused

  Debug checklist:
    1. Is the OTel Collector running?
    2. Is the endpoint correct in .env?
    3. Is there a firewall or network policy blocking traffic?
    4. If using Docker Compose, are both services on the same network?
==============================================================

Trace structure

When called from another instrumented service, a single trace shows:

[calling-service] POST /mcp (HTTP client span)
  |
  +-- [playwright-mcp] POST /mcp (ASGI server span, auto)
      |
      +-- [playwright-mcp] mcp.tool.browser_navigate (manual span)

Without tracing

If OTEL_EXPORTER_OTLP_ENDPOINT is not set, the OTel SDK initializes in no-op mode -- zero overhead, no traces exported. The server works exactly as before.

References

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tomscoppock/playwright_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server