playwright-secure-mcp

Extend the Playwright MCP to allow safe password handling using 1Password

playwright-secure-mcp

A Crystal MCP server that transparently proxies the upstream Playwright MCP server (@playwright/mcp) and adds secure password handling. Its goal: a resolved secret value never reaches the LLM.

How it works

  • The binary speaks MCP JSON-RPC 2.0 over stdio to the client (the LLM host) and spawns @playwright/mcp as a stdio child process, forwarding nearly all messages untouched.
  • It adds four secret tools to the upstream tools/list: three discovery tools that resolve 1Password items into identities (IDs), and browser_type_secret, which types a field of such an item into the page. See Secret tools.
  • Every message flowing back to the client is passed through a redactor that replaces each resolved secret — including its URL-encoded, Base64, HTML-escaped, and JSON-escaped variants — with the literal token «REDACTED». Secrets are caught wherever they appear: page snapshots, network request dumps, console messages, and error text.

Secret tools

Discovery: find an item's IDs

Three tools look up 1Password items and return only their identities as a JSON array of {"vault": ..., "item": ..., "title": ...} objects (plus "url" for items that have one) — never a secret value. All three accept an optional vault (ID or name) to scope the search.

Tool Required arguments Result
browser_find_secret_by_name item (ID or name) The matching item
browser_find_secret_by_tag tag All items carrying the tag
browser_find_secret_by_url url Login items matching the given page URL

browser_find_secret_by_url takes the page url as an argument and ranks login items by domain and longest matching path. It queries 1Password only — it never takes a page snapshot or calls the upstream server.

Typing: browser_type_secret

Mirrors the upstream browser_type tool, but instead of a literal text value it takes the 1Password coordinates of the secret:

  • Required: element, ref (as in browser_type), vault (1Password vault ID), item (1Password item ID), field (e.g. username or password)
  • Optional: submit, slowly (as in browser_type)

The proxy assembles op://vault/item/field, resolves it locally via op read, and issues an internal browser_type call to the upstream server with the resolved value.

Workflow: find, then type

  1. Call a discovery tool to obtain the vault and item IDs — e.g. pass the login page URL to browser_find_secret_by_url.
  2. Call browser_type_secret with those IDs and the field to type.

Install

Homebrew (macOS and Linux)

Install from the Homebrew tap. The formula installs the prebuilt binary from the latest GitHub release (the macOS builds are signed and notarized):

brew install jochenseeber/tap/playwright-secure-mcp

This puts playwright-secure-mcp on your PATH. It still needs the 1Password CLI (op) and a Playwright MCP server — see Requirements.

To build from source instead, see Build.

Requirements

  • Crystal >= 1.20
  • The 1Password CLI (op), signed in
  • pnpm or npm to download @playwright/mcp on demand, or a pre-installed Playwright MCP server binary

Build

rake setup
rake build

rake build writes a debug binary to bin/<profile>-<mode>/playwright-secure-mcp (e.g. bin/darwin-arm64-system-dynamic-debug/…) and a bin/playwright-secure-mcp symlink to the latest build. Use rake "build[release]" for a release binary. Run rake -T to list all available tasks.

Options

Option Default Meaning
--package-manager pnpm pnpm (pnpm dlx), npm (npx -y), or none (pre-installed)
--mcp-version latest @playwright/mcp version tag/range (ignored when none)
--mcp-bin mcp-server-playwright Pre-installed binary; implies --package-manager none
--command (none) Explicit upstream command override
--op-command op 1Password CLI binary
--account-from-git (none) Read the account email from DIR/.git/config (user.email)
--account (none) 1Password account (shorthand, sign-in email, or account ID)
--account-email (none) 1Password account email
--token-tag (none) 1Password item tag whose credential field holds a service-account token
--require-hardware-key (off) Refuse to start without Secure Enclave/TPM key protection
-- <args...> (none) Extra args forwarded to the upstream server

The three account options resolve to a single account passed to op via --account; when more than one is given, precedence is --account-from-git > --account-email > --account. With --token-tag, the resolved account is used once at startup (interactive op) to fetch the tagged item's credential field, and that value is then used as OP_SERVICE_ACCOUNT_TOKEN for all subsequent secret resolution — in that mode --account is not passed to op read. Without --token-tag, each op read uses the resolved account directly.

MCP client configuration

{
    "mcpServers": {
        "playwright": {
            "command": "/path/to/bin/playwright-secure-mcp",
            "args": ["--", "--headless"]
        }
    }
}

Security model

  • The resolved secret travels op → this process → upstream child → browser. It is never present in anything the LLM sent, and never present un-redacted in anything the LLM receives.
  • Resolved secrets are cached in-memory for the process lifetime in an obfuscated vault: keys are SHA-256 hashes of the op:// reference, values are AES-256-CBC encrypted under a random per-process data key with a fresh random IV per entry. The data key itself is hardware-protected when possible — see Cache key protection.
  • Caveat: the vault is obfuscation / defense-in-depth, not a security boundary. With the in-memory fallback tier the encryption key lives in the same process memory as the ciphertext, so it defeats casual heap inspection, strings-style scanning, and accidental plaintext logging — but not an attacker with full process-memory access. A hardware-backed tier removes the long-lived key from process memory, but see the limitations below.

Cache key protection

At startup the proxy picks the best available protection tier for the vault's AES-256 data key and logs the choice:

  1. Secure Enclave (macOS, hardware): an ephemeral, non-extractable P-256 key is generated inside the Secure Enclave, and the data key is ECIES-wrapped under it. The wrapped key is unwrapped in the enclave per crypto batch and the plaintext key is zeroed afterwards; the long-lived key never exists in process memory or on disk.
  2. TPM 2.0 (Linux, hardware): the data key is sealed inside the platform TPM via the tpm2-tss ESYS library over /dev/tpmrm0 and unsealed transiently per crypto batch, then zeroed. The binary links tpm2-tss, which is assumed present on the host.
  3. Kernel keyring (Linux, not hardware): the data key is stored in the kernel keyring and AES runs in the kernel via an AF_ALG socket, so the key never re-enters process memory — but it is kernel-backed, not hardware-backed. Requires Linux ≥ 5.4.
  4. In-memory (fallback): a plain per-process key, with a startup warning that hardware-backed protection is unavailable.

On Linux the order is TPM → keyring → in-memory. Pass --require-hardware-key to fail closed: the proxy refuses to start unless a hardware-backed tier (Secure Enclave / TPM) initializes. The kernel keyring tier does not satisfy --require-hardware-key.

Deployment requirement (macOS): the Secure Enclave is only usable when the distributed binary is code-signed with the appropriate entitlement (Secure Enclave / keychain access). An unsigned binary cannot generate an enclave key (Security.framework fails with OSStatus -26276) and falls back to the in-process key — or refuses to start under --require-hardware-key.

Limitations:

  • The unwrapped data key is in process memory transiently per crypto batch, then best-effort zeroed.
  • Decrypted secret plaintext still transits process memory during redaction and typing (out of scope; would require a sidecar).
  • Per-message hot-path cost with the Secure Enclave is ~2–20 ms (one key unwrap per message batch).

Tests

rake spec
rake lint
Repository

playwright-secure-mcp

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 2
  • about 6 hours ago
  • July 13, 2026
License

MIT License

Links
Synced at

Thu, 16 Jul 2026 00:34:32 GMT

Languages