Skip to main content
Glama
daninden

playwright-mcp-azure

by daninden

playwright-mcp-azure

A remote MCP server that gives an MCP client general-purpose headless-browser control via Playwright, exposed over Streamable HTTP and deployable to Azure Container Apps.

A single process holds one shared headless Chromium browser. Each client connection gets its own Playwright BrowserContext + active Page, tracked by an in-memory session manager with idle eviction and a concurrency cap. Sessions are in-memory only — nothing persists across a restart.

Prerequisites

  • Node.js 20+

  • npm

  • Docker (only needed for building/deploying the container)

  • Azure CLI (only needed for deploy)

Related MCP server: Playwright MCP Server

Local setup

npm install
npx playwright install chromium   # one-time, downloads the Chromium binary

The server requires a bearer token for auth:

export MCP_BEARER_TOKEN=some-local-dev-token   # PowerShell: $env:MCP_BEARER_TOKEN = "..."
npm run dev

This starts the server on http://localhost:3000, with the MCP endpoint mounted at /mcp (Streamable HTTP) and an unauthenticated health check at /healthz.

Environment variables

Variable

Required

Default

Description

MCP_BEARER_TOKEN

yes

Bearer token clients must send as Authorization: Bearer <token> on /mcp.

IDLE_TIMEOUT_MS

no

600000

Milliseconds of inactivity before a session's browser context is closed.

MAX_SESSIONS

no

5

Maximum concurrent browser sessions.

PORT

no

3000

Port the HTTP server listens on.

PWD_<hostname>

no

Password browser_login fills on https://<hostname> pages, e.g. PWD_www.linkedin.com. Values shorter than 6 characters are ignored with a warning.

Running tests

npm test

Integration tests launch real headless Chromium instances, so the Playwright browser install step above is required first.

Building

npm run build   # compiles src/ to dist/
npm start        # runs the compiled server (dist/server.js)

Connecting an MCP client

Point an MCP client that supports Streamable HTTP at http://<host>:<port>/mcp, sending Authorization: Bearer <MCP_BEARER_TOKEN> on every request. The server exposes these tools:

browser_start_session, browser_navigate, browser_navigate_back, browser_click, browser_type, browser_fill_form, browser_login, browser_hover, browser_press_key, browser_select_option, browser_handle_dialog, browser_snapshot, browser_find, browser_screenshot, browser_evaluate, browser_wait_for, browser_tabs, browser_network_requests, browser_console_messages, browser_close.

Browser sessions are identified by an app-level sessionId, not by the MCP transport's own session id -- some MCP clients (notably Azure AI Foundry's agent tool integration) open a brand-new transport connection for every single tool call and never reuse the transport-level session, so nothing tied to that id would survive between calls. Call browser_start_session once at the start of a task to get a sessionId, then pass that same sessionId as an argument on every other browser_* call for that task. If a call returns an error naming browser_start_session, the session expired or was closed (idle timeout, or an explicit browser_close) -- start a new one and resume from there.

browser_snapshot returns a role/name listing of interactive elements, each stamped with a ref (e.g. e3); pass that ref to browser_click/browser_type/browser_fill_form to act on that element.

browser_network_requests records XHR and fetch GET/POST requests across every tab in the session (up to the last 200; older entries drop off). Call it with { "action": "list" } for { url, method, requestBody, status, responseBody } per request, optionally adding "urlPattern": "*/api/*" to filter by a *-wildcard match on the URL, or { "action": "clear" } to empty the log.

Logging in without exposing the password

browser_login fills the current page's password field with the password stored for that page's hostname, without the value ever reaching the model. It takes only a sessionId: the hostname of the current page selects the secret. It refuses — filling nothing and saying why — unless all three hold:

  • the page is served over https;

  • a PWD_<hostname> secret exists for the page's exact hostname (no parent-domain fallback: a secret for linkedin.com is not used on www.linkedin.com);

  • the page's main frame shows exactly one visible input[type="password"].

The tool only fills the password. Type the username yourself with browser_type first, and submit the form yourself afterwards.

Every stored password is also a redaction pattern. Before any tool result leaves the server, each text item is scanned for every stored value — raw, JSON-escaped, and percent-encoded — and each occurrence is replaced with [REDACTED]. That covers browser_snapshot, browser_find, browser_evaluate, browser_network_requests (whose captured request and response bodies would otherwise contain the login POST), error messages, and the server's own logs. Screenshots are not scrubbed, but browsers mask password inputs themselves.

Tool failures are never reported via the MCP protocol's isError flag -- Azure AI Foundry's agent orchestration treats an isError result as a fatal tool-call failure and stops the run instead of letting the model see and react to it. Instead, a failed call still returns a normal result whose content is a JSON object like { "error": "<description>" }, so the model can read the error and decide how to proceed (retry, call browser_start_session again, etc.).

Deploying to Azure Container Apps

Infrastructure (infra/main.bicep) provisions an Azure Container Registry, a Log Analytics workspace, a Container Apps environment, and a single Container App (HTTPS-only ingress, pinned to 1 replica). It needs a container image to deploy, and the registry doesn't exist until the infra is applied — so the first deploy provisions with a placeholder image, then infra/deploy.ps1 builds and pushes the real one.

  1. Log in and create a resource group (skip if you already have one):

    az login
    az group create --name rg-playwright-mcp --location westeurope
  2. Provision the infrastructure with a placeholder image:

    az deployment group create \
      --resource-group rg-playwright-mcp \
      --template-file infra/main.bicep \
      --parameters mcpBearerToken=<a-strong-random-token> \
                   containerImage=mcr.microsoft.com/k8se/quickstart:latest

    Note the acrLoginServer output (or a name prefix segment of it) — that's the -AcrName value for the next step.

  3. Build, test, and deploy the real image:

    ./infra/deploy.ps1 -ResourceGroup rg-playwright-mcp -AcrName <acr-name-from-step-2>

    This runs npm ci, installs the Chromium browser, builds, runs the test suite, then builds and pushes a Docker image and updates the Container App to use it. Re-run this script for subsequent deploys — the infra step only needs to run once (or again if infra/main.bicep changes).

  4. Get the URL:

    az containerapp show --name playwright-mcp-app --resource-group rg-playwright-mcp \
      --query properties.configuration.ingress.fqdn -o tsv

    The MCP endpoint is https://<fqdn>/mcp.

To change IDLE_TIMEOUT_MS, MAX_SESSIONS, or PORT in Azure, edit their values in infra/main.bicep's container env block and redeploy the infra step.

Storing a password for browser_login

./infra/set-login-secret.ps1 -ResourceGroup rg-playwright-mcp -Domain www.linkedin.com

It prompts for the password (so it stays out of your shell history), stores it as a Container Apps secret named pwd-www-linkedin-com, and points the PWD_www.linkedin.com environment variable at that secret. Both steps create a new revision, which restarts the container — the server reads its secrets once at startup. Add -Remove to delete a stored password.

Warning: stored passwords do not survive a bicep deploy. infra/main.bicep declares the full secrets and env arrays, so re-running the az deployment group create step in this section resets them to what the template lists and drops every pwd-* secret. Re-run set-login-secret.ps1 for each domain afterwards. infra/deploy.ps1 is safe — it only calls az containerapp update --image, which leaves secrets and environment variables untouched.

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    Enables web browser automation through Playwright, providing tools for navigation, element interaction, screenshot capture, and accessibility snapshots. Uses streamableHttp transport for seamless integration with MCP clients.
    6
    673 npm
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides browser automation capabilities via HTTP endpoints by wrapping the official Playwright MCP package, enabling serverless deployments and cloud environments where STDIO-based communication is not possible.
    1
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables browser automation and web scraping by exposing Playwright tools through an HTTP-based MCP server. Users can navigate pages, interact with web elements, capture screenshots, and extract structured content using a persistent Chromium instance.
    MIT