Skip to content
archived Visibility internal Owner erik@uvilo.com Approver _ Created _ Updated _

Uvilo OS Playwright MCP Research

Problem

The LibreChat instance on Railway needs web browsing capabilities equivalent to the “Claude in Chrome” setup on the local Mac. The AI assistant should be able to navigate to URLs, read page content, interact with web elements, and extract information from JavaScript-heavy sites.

Candidate: Microsoft Playwright MCP (@playwright/mcp)

The clear winner for this use case. It’s the official Microsoft Playwright MCP server, actively maintained (GitHub: microsoft/playwright-mcp), and purpose-built for giving LLMs browser control.

Key Features

  • Accessibility snapshot mode (default): Uses the browser’s accessibility tree — a structured, text-based representation of the page — instead of screenshots. Fast, token-efficient, no vision model needed.
  • Vision mode (optional): Takes screenshots for complex visual layouts. Requires a vision-capable model.
  • Full Playwright API: Navigate, click, type, take screenshots, handle dialogs, inspect network activity, run JavaScript, manage tabs, upload/download files.
  • Headless Chromium in Docker: Official image at mcr.microsoft.com/playwright/mcp ships with Chromium and all dependencies pre-installed.
  • HTTP transport: Supports --port flag to expose an HTTP/MCP endpoint that any MCP client can connect to via URL.

Tools Provided (Core)

ToolDescription
browser_navigateNavigate to a URL
browser_clickClick an element by accessibility ref
browser_typeType text into form fields
browser_snapshotGet accessibility tree snapshot of current page
browser_screenshotTake a screenshot (vision mode)
browser_tab_*Tab management (new, close, select, list)
browser_network_requestsInspect network activity
browser_console_messagesRead browser console
browser_file_uploadUpload files
browser_pdf_saveSave page as PDF
browser_evaluateRun JavaScript in page context

Deployment Options Evaluated

Option A: Sidecar Service on Railway (Selected ✅)

Deploy the Playwright MCP Docker image as a separate Railway service in the same project. LibreChat connects to it over Railway’s private network via HTTP URL.

Architecture:

┌──────────────────────────────────────────────────────────┐
│  Railway Project: uvilo-libre-chat                       │
│                                                          │
│  ┌──────────────┐   HTTP/MCP    ┌─────────────────────┐ │
│  │  LibreChat    │──────────────►│ Playwright MCP      │ │
│  │              │  (private net) │ (headless Chromium)  │ │
│  │              │               │                       │ │
│  │  stdio MCP:  │               │ Image:                │ │
│  │  - filesystem│               │ mcr.microsoft.com/    │ │
│  │  - git       │               │ playwright/mcp        │ │
│  │  - git-remote│               │                       │ │
│  └──────────────┘               │ Port: 8931            │ │
│                                 │ Host: 0.0.0.0         │ │
│                                 └─────────────────────┘ │
│                                                          │
│  Connection: http://playwright-mcp.railway.internal:8931 │
└──────────────────────────────────────────────────────────┘

Pros:

  • Clean separation of concerns — browser process is isolated
  • Uses official pre-built Docker image (zero custom build)
  • Independent resource allocation (memory/CPU)
  • LibreChat container stays lean
  • Easy to restart/update independently

Cons:

  • Adds a service to the Railway project (cost ~$5-10/mo idle)
  • Requires Railway private networking (already available)
  • Need to configure mcpSettings.allowedDomains in librechat.yaml

Railway Service Configuration:

  • Service name: playwright-mcp
  • Image: mcr.microsoft.com/playwright/mcp
  • Custom Start Command: node cli.js --headless --browser chromium --no-sandbox --port 8931 --host 0.0.0.0 (Railway’s start command overrides the image’s ENTRYPOINT in exec form — no separate entrypoint field)
  • Serverless: Enabled — service sleeps after 10 min of inactivity, wakes on private network traffic from LibreChat. Cold start ~5–15s (Chromium is heavy); covered by initTimeout: 30000 in librechat.yaml.
  • No public domain needed (internal only)
  • Memory: 512MB–1GB recommended

LibreChat config (librechat.yaml):

mcpSettings:
  allowedDomains:
    - "playwright-mcp.railway.internal"

mcpServers:
  playwright:
    title: "Web Browser"
    description: "Browse the web, read pages, interact with sites"
    type: sse
    url: http://playwright-mcp.railway.internal:8931/sse
    timeout: 60000
    initTimeout: 60000
    serverInstructions: |
      Headless Chromium browser for web browsing, research, and interaction.
      Use browser_navigate to visit URLs, browser_snapshot to read page content.
      Pages are returned as accessibility tree snapshots (structured text),
      not screenshots. Use browser_click and browser_type for interaction.
      Browser sessions are ephemeral — no persistent login state.

Note: SSE vs Streamable-HTTP — The /mcp endpoint (streamable-http) requires OAuth per the MCP spec. Since the Playwright service is on Railway’s private network, the user’s browser can’t reach it for the OAuth redirect flow. The /sse endpoint is the legacy transport that doesn’t require OAuth and works for server-to-server connections.

Note: Allowed Hosts — The @playwright/mcp image has a separate access control list (--allowed-hosts) that defaults to the bound host (localhost). Even with --host 0.0.0.0, connections from Railway private network IPs are rejected with HTTP 403 unless --allowed-hosts * is set. LibreChat misinterprets this 403 as an OAuth error.

⚠️ Env var name bug (microsoft/playwright-mcp#1373): The README documents PLAYWRIGHT_MCP_ALLOWED_HOSTS but the actual source code reads PLAYWRIGHT_MCP_ALLOWED_HOSTNAMES. Use the correct name on Railway: PLAYWRIGHT_MCP_HOST=0.0.0.0 and PLAYWRIGHT_MCP_ALLOWED_HOSTNAMES=*. CLI flags may be swallowed by the Docker ENTRYPOINT.

Option B: Embedded via npx (Rejected)

Add Chromium to the custom LibreChat Docker image (ghcr.io/erikdakoda/librechat-git) and run Playwright MCP as a stdio process alongside the other MCP servers.

Rejected because:

  • Docker image becomes much larger (+400MB for Chromium)
  • Browser and LibreChat compete for memory in one container
  • Every redeploy must re-download/build the larger image
  • Harder to debug browser issues vs app issues

Option C: Cloud Browser Service (Deferred)

Use a hosted browser service like Browser Use Cloud, Browserbase, or Browserless. These provide remote browser instances via API/MCP.

Deferred because:

  • Monthly cost ($29+/mo minimum)
  • External dependency and latency
  • Not needed for current scale

Worth revisiting if anti-detection or multi-browser concurrency becomes necessary.


Implementation Steps

  1. Create new Railway service playwright-mcp in the uvilo-libre-chat project
  2. Set Docker image to mcr.microsoft.com/playwright/mcp
  3. Set Custom Start Command to: node cli.js --headless --browser chromium --no-sandbox --port 8931 --host 0.0.0.0 (this overrides the image’s ENTRYPOINT — no separate entrypoint field in Railway)
  4. Enable Serverless in Settings → Deploy (sleeps after 10 min, wakes on traffic)
  5. Verify it starts and is reachable on the private network
  6. Update librechat.yaml on dev branch with the new MCP server config
  7. Deploy config: cp .../Assistant/Configs/librechat.yaml /workspace/librechat/
  8. Redeploy LibreChat (railway redeploy --service LibreChat)
  9. Test: ask the assistant to navigate to a URL and read the page

Estimated effort: 30–60 minutes


Key Decision: Railway Private Networking

Railway services in the same project communicate over an encrypted Wireguard tunnel using internal DNS names (<service-name>.railway.internal). No public domain is needed for the Playwright MCP service — LibreChat connects directly over the private network.

Important: internal domains require mcpSettings.allowedDomains in librechat.yaml to whitelist the internal hostname, or LibreChat will refuse to connect.


References