Skip to main content
Glama

Camoufox Canary MCP

An experimental, single-container MCP service for creating isolated Camoufox browser sessions and connecting Playwright to them through one published port. It is intended for a trusted HomeLAN with fewer than ten users.

The service exposes:

  • Streamable HTTP MCP at http://HOST:8000/mcp

  • proxied browser WebSockets at ws://HOST:8000/browser/SESSION_ID

  • an unauthenticated health check at http://HOST:8000/health

Quickstart

Docker Compose is the shortest supported path. Port 8000 is intentionally fixed in Compose so the published mapping, healthcheck, and service listener agree.

Copy the fully commented example configuration, then edit at least the public URL for access from another machine. Setting a bearer token is recommended:

cp .env.example .env
${EDITOR:-vi} .env
docker compose up --build --detach --wait
curl --fail http://127.0.0.1:8000/health

The expected health response is {"status":"ok"}. Stop and remove the service with:

docker compose down

To build the image used by the end-to-end test:

docker build -t camoufox-canary-mcp:test .

For another machine on the LAN, set the public URL to the address its clients can reach before starting Compose:

CAMOUFOX_MCP_PUBLIC_URL=ws://192.168.1.20:8000 docker compose up --build --detach --wait

The image runs as the unprivileged app user (UID/GID 10001) and publishes only the MCP service port. Browser ports 9100–9109 remain private inside the container.

Related MCP server: camofox-browser-mcp

Manual Docker deployment

Build the image and create a local environment file from the documented template:

docker build -t camoufox-canary-mcp:local .
cp .env.example .env
${EDITOR:-vi} .env

In .env, replace localhost in CAMOUFOX_MCP_PUBLIC_URL with the Docker host address reachable by MCP clients, for example 192.168.1.20. Start the container with an init process and 2 GiB of shared memory:

docker run --detach \
  --name camoufox-canary-mcp \
  --init \
  --shm-size=2g \
  --publish 8000:8000 \
  --env-file .env \
  --health-cmd="python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)\"" \
  --health-interval=30s \
  --health-timeout=5s \
  --health-retries=3 \
  camoufox-canary-mcp:local

Check startup, health, and logs:

docker ps --filter name=camoufox-canary-mcp
curl --fail http://127.0.0.1:8000/health
docker logs --follow camoufox-canary-mcp

Stop and remove the manual deployment with:

docker stop camoufox-canary-mcp
docker rm camoufox-canary-mcp

To publish a different host port, change only the left side of the mapping, for example --publish 18000:8000, and set the public URL to ws://HOST:18000. Keep CAMOUFOX_MCP_LISTEN_PORT=8000 inside the container. Do not publish the private browser port range.

Configuration

All variables are optional. Compose deliberately does not pass through CAMOUFOX_MCP_LISTEN_PORT; its container-side value must remain 8000. See .env.example for a copy-ready configuration containing every variable, valid values, examples, and operational notes.

Variable

Default

Valid values and meaning

CAMOUFOX_MCP_LISTEN_HOST

0.0.0.0

Non-empty Uvicorn bind address.

CAMOUFOX_MCP_LISTEN_PORT

8000

Integer 1–65535. Keep 8000 inside this image and with Compose.

CAMOUFOX_MCP_PUBLIC_URL

ws://localhost:8000

Client-reachable ws:// or wss:// base placed in session results. A trailing slash is stripped.

CAMOUFOX_MCP_AUTH_TOKEN

unset

Non-blank optional bearer token protecting MCP and browser WebSocket requests. /health stays open.

CAMOUFOX_MCP_MAX_SESSIONS

4

Positive integer maximum for tracked browser sessions. Effective capacity cannot exceed the private port count.

CAMOUFOX_MCP_IDLE_TIMEOUT_SECONDS

900

Positive integer seconds since the last proxied WebSocket traffic before cleanup.

CAMOUFOX_MCP_STARTUP_TIMEOUT_SECONDS

30

Positive integer seconds allowed for the private browser server to bind.

CAMOUFOX_MCP_CLEANUP_INTERVAL_SECONDS

30

Positive integer seconds between idle-session cleanup passes.

CAMOUFOX_MCP_PORT_START

9100

Integer 1–65535; first private browser port.

CAMOUFOX_MCP_PORT_END

9109

Integer 1–65535; last private browser port, greater than or equal to PORT_START.

CAMOUFOX_MCP_HEADLESS

virtual

Exactly virtual (per-session Xvfb) or native (Firefox native headless).

CAMOUFOX_MCP_ALLOWED_HOSTS

*

Comma-separated MCP Host values. * disables SDK DNS-rebinding protection; use explicit client-visible hosts in less-trusted networks.

CAMOUFOX_MCP_ALLOWED_ORIGINS

empty

Comma-separated MCP Origin values allowed when DNS-rebinding protection is enabled.

The private port range is inclusive. Configure at least as many ports as the desired session count; otherwise the port range becomes the effective limit. All integer settings reject zero, negative, and non-integer values. Port values outside 1–65535 are also rejected. Unknown environment variables are ignored.

Example authenticated Compose start:

CAMOUFOX_MCP_AUTH_TOKEN='replace-this-token' \
CAMOUFOX_MCP_PUBLIC_URL='ws://192.168.1.20:8000' \
docker compose up --build --detach --wait

Send Authorization: Bearer replace-this-token on both MCP HTTP requests and the Playwright WebSocket connection. The token and any proxy credentials are never returned in session metadata.

MCP tools

create_session accepts this input shape; every field is optional:

{
  "proxy_server": "http://proxy.example:8080",
  "proxy_username": "user",
  "proxy_password": "password",
  "os": "linux",
  "locale": "en-AU",
  "timezone": "Australia/Sydney",
  "screen": {
    "min_width": 1280,
    "max_width": 1920,
    "min_height": 720,
    "max_height": 1080
  }
}
  • proxy_server accepts http, https, or socks5. Username and password must be supplied together and separately from the URL.

  • os is windows, macos, or linux.

  • locale must be a registered language tag such as en-AU, en-US, or de-DE.

  • timezone must be an IANA timezone available in the image, such as Australia/Sydney, America/New_York, or Etc/UTC.

  • Every screen bound is a positive integer; each minimum must not exceed its maximum.

  • Unknown fields are rejected. Supplying a proxy username without a password, or vice versa, is rejected. Proxy credentials embedded in the URL are also rejected; use the separate credential fields.

It returns {id, websocket_url, created_at, expires_at, request}. The other tool schemas are:

Tool

Input

Result

list_sessions

{}

{"result": [session, ...]}

get_session

{"session_id": "..."}

one session object

close_session

{"session_id": "..."}

{"id": "...", "closed": true}

Connect MCP clients

Replace 192.168.1.20 with the Docker host address reachable from the client. When CAMOUFOX_MCP_AUTH_TOKEN is set on the server, export the same value in the shell that starts your MCP client:

export CAMOUFOX_MCP_AUTH_TOKEN='replace-this-token'

Do not commit the real token. Omit the authorization setting entirely when server authentication is disabled. Use an https:// MCP URL when TLS is terminated by a reverse proxy.

Claude Code

Add the server for the current project without authentication:

claude mcp add --transport http --scope project camoufox \
  http://192.168.1.20:8000/mcp

For bearer authentication, create .mcp.json in the project root. Claude Code expands ${CAMOUFOX_MCP_AUTH_TOKEN} from its environment:

{
  "mcpServers": {
    "camoufox": {
      "type": "http",
      "url": "http://192.168.1.20:8000/mcp",
      "headers": {
        "Authorization": "Bearer ${CAMOUFOX_MCP_AUTH_TOKEN}"
      }
    }
  }
}

Run claude mcp list, or use /mcp inside Claude Code, to check the connection. See the official Claude Code MCP documentation.

Codex CLI

Add an unauthenticated server:

codex mcp add camoufox --url http://192.168.1.20:8000/mcp

For bearer authentication, tell Codex which environment variable contains the token:

codex mcp add camoufox \
  --url http://192.168.1.20:8000/mcp \
  --bearer-token-env-var CAMOUFOX_MCP_AUTH_TOKEN

The equivalent ~/.codex/config.toml entry is:

[mcp_servers.camoufox]
url = "http://192.168.1.20:8000/mcp"
bearer_token_env_var = "CAMOUFOX_MCP_AUTH_TOKEN"

Remove bearer_token_env_var when authentication is disabled. Run codex mcp list, or use /mcp inside Codex, to check the connection. See the official Codex MCP documentation.

OpenCode

Add this entry to opencode.json for an authenticated connection. OpenCode uses {env:NAME} for environment-variable substitution:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "camoufox": {
      "type": "remote",
      "url": "http://192.168.1.20:8000/mcp",
      "enabled": true,
      "oauth": false,
      "headers": {
        "Authorization": "Bearer {env:CAMOUFOX_MCP_AUTH_TOKEN}"
      }
    }
  }
}

Remove the headers object and oauth when authentication is disabled. Run opencode mcp list to check the connection. See the official OpenCode MCP documentation.

These integrations expose the session-management tools listed above. After create_session returns a websocket_url, browser automation connects to that URL with Playwright as shown in the following examples.

Python client

Install the client packages at the same versions as the service, then initialize MCP, create a session, and connect Playwright Firefox to the returned URL:

python -m pip install 'mcp==2.0.0' 'playwright==1.60.0'
import asyncio

import httpx2
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
from playwright.async_api import async_playwright


async def main() -> None:
    token = "replace-this-token"  # Use None when authentication is disabled.
    headers = {"Authorization": f"Bearer {token}"} if token else {}

    async with httpx2.AsyncClient(headers=headers) as http:
        async with streamable_http_client(
            "http://127.0.0.1:8000/mcp", http_client=http
        ) as (read, write):
            async with ClientSession(read, write) as mcp:
                await mcp.initialize()
                result = await mcp.call_tool("create_session", {})
                session = result.structured_content
                assert session is not None
                try:
                    async with async_playwright() as playwright:
                        browser = await playwright.firefox.connect(
                            session["websocket_url"], headers=headers
                        )
                        page = await browser.new_page()
                        await page.goto("data:text/html,<title>Camoufox</title>")
                        print(await page.title())
                        await browser.close()
                finally:
                    await mcp.call_tool(
                        "close_session", {"session_id": session["id"]}
                    )


asyncio.run(main())

Connecting does not require downloading a local Firefox binary; the browser runs in the container.

Node Playwright connection

Use the same Playwright version as the image:

npm install 'playwright@1.60.0'

After an MCP create_session call has supplied CAMOUFOX_WS_URL:

const { firefox } = require("playwright");

(async () => {
  const token = process.env.CAMOUFOX_MCP_AUTH_TOKEN;
  const headers = token ? { Authorization: `Bearer ${token}` } : {};
  const browser = await firefox.connect(process.env.CAMOUFOX_WS_URL, { headers });
  const page = await browser.newPage();
  await page.goto("data:text/html,<title>Camoufox</title>");
  console.log(await page.title());
  await browser.close();
})();

Closing the Playwright client is not a substitute for close_session; call the MCP tool to release the managed session and private port.

Session lifecycle and errors

Each successful create_session starts one private Camoufox process and reserves one private port. A session remains tracked until close_session, idle cleanup, application shutdown, or container termination closes that process. WebSocket traffic refreshes its idle timer; MCP metadata reads do not.

  • Creation fails when MAX_SESSIONS is reached or no configured private port remains. Failed startups release their reservation.

  • Unknown session IDs fail get_session, close_session, and WebSocket connection attempts.

  • If browser termination fails, the session remains tracked so cleanup can retry and its port cannot be assigned unsafely.

  • Session state is memory-only. Restarting the container closes and forgets all sessions.

  • Invalid session options are rejected before browser startup. Proxy-related errors are intentionally generic so submitted credentials cannot leak.

  • HTTP 401 means a bearer credential is absent or malformed; 403 means the supplied token does not match. WebSocket upgrades are rejected under the equivalent conditions.

TLS reverse proxy

The service does not terminate TLS. A reverse proxy must forward normal HTTP, Streamable HTTP responses, and WebSocket upgrades to the same port. For example, with Caddy:

camoufox.example.lan {
    tls internal
    reverse_proxy 127.0.0.1:8000
}

Run the container with:

CAMOUFOX_MCP_PUBLIC_URL=wss://camoufox.example.lan \
CAMOUFOX_MCP_ALLOWED_HOSTS=camoufox.example.lan \
CAMOUFOX_MCP_ALLOWED_ORIGINS=https://camoufox.example.lan \
docker compose up --build --detach --wait

Clients then use https://camoufox.example.lan/mcp for MCP and receive wss://camoufox.example.lan/browser/SESSION_ID from create_session. Keep bearer authentication enabled unless the proxy and network independently restrict every client. Certificate trust for Caddy's internal CA must be installed on each client.

Virtual-headless behavior

The default virtual mode gives each session its own Xvfb display while Firefox runs in headful mode against that display. This preserves the virtual-headless behavior of the pinned canary without exposing an X11 port. Camoufox generates a coherent fingerprint automatically, enables humanized input, uses its bundled uBlock Origin only, and enables GeoIP alignment when a proxy is supplied.

Set CAMOUFOX_MCP_HEADLESS=native only when Firefox's native headless mode is preferred over the stealth-oriented default.

Verification

Install the locked test environment and run the unit suite (the Docker E2E is collected but skipped by default):

uv sync --group test
.venv/bin/python -m pytest -q
docker compose config

Build the named image and opt in to the real non-root, virtual-headless MCP and Playwright WebSocket test:

docker build -t camoufox-canary-mcp:test .
CAMOUFOX_MCP_RUN_E2E=1 \
CAMOUFOX_MCP_E2E_IMAGE=camoufox-canary-mcp:test \
.venv/bin/python -m pytest tests/e2e/test_container.py -v

The E2E always removes its test container, including after failures.

Upstream pins and updates

The build checks out Camoufox PR #707 at d83cbb373b63ffc063e068d89efcf78620ba1a53, then cherry-picks the integrity verification change from PR #716 at 65cda21b4fc10d8eeaec46d8f82c1b2a9fbb0791.

To update, first verify the candidate revisions are compatible. Then change the two constants in scripts/build-camoufox.sh, the matching OCI labels in the Dockerfile, and the assertions in tests/test_build_pins.py. Rebuild the test image and run the unit suite, docker compose config, and the opt-in E2E command above before using the new image.

Limitations and troubleshooting

  • This is a canary build from unmerged upstream revisions. State is in memory; restarting the single process closes all sessions. There is no built-in TLS, persistence, multi-host coordination, or browser-action MCP tool set.

  • A healthy service with a failing create_session usually needs more startup time or memory, or the requested fingerprint constraints may be incompatible. Keep the 2 GiB shared-memory setting and inspect the child-process diagnostic with docker compose logs --tail=200 camoufox-canary-mcp.

  • HTTP 401 means the bearer header is missing or malformed; 403 means the token is wrong. The same header is required in Playwright connect options.

  • A Playwright protocol/version error usually means the client differs from 1.60.0. Match the pinned version shown above.

  • If session URLs point at localhost from another computer, set CAMOUFOX_MCP_PUBLIC_URL to the LAN-reachable ws:// or reverse-proxied wss:// address.

  • For non-local MCP deployments, set explicit comma-separated CAMOUFOX_MCP_ALLOWED_HOSTS and CAMOUFOX_MCP_ALLOWED_ORIGINS values that match the client-visible host and origin.

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Browser MCP for logged-in tasks. Uses your Chrome — credentials stay local. Zero-token replay.

  • Hosted real Google Chrome MCP with per-user persistent state. Navigate, click, type, screenshot.

  • MCP (Model Context Protocol) server for Appwrite

View all MCP Connectors

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/midnightphreaker/CamouFox-Canary-MCP'

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