playwright-secure-mcp

围绕 Playwright MCP 服务器的封装,旨在防止 AI 获取机密信息

文档

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 accidentally 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 list or find 1Password LOGIN items usable on the current browser page, and browser_type_secret, which types a field of a chosen item into the page. Closing the browser (browser_close) empties the local item cache. 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: list or find items for the current page

Three tools look up 1Password LOGIN items and return only those usable on the current browser page: the proxy reads the page's location.href itself (the caller never supplies a URL) and keeps an item only when one of its URLs matches the page by host and path prefix. If the current URL cannot be determined, discovery fails with an error. Results are a JSON array of item identities plus non-secret field metadata — vault, item, title, urls, tags, fields (id, label, type, purpose, section), and sections — never a field value. All three accept an optional vault (ID or name) to scope the search.

ToolRequired argumentsResult
browser_list_items(none)All LOGIN items usable on the current page
browser_find_items_by_nameitem (title or ID)Matching items, filtered to the current page
browser_find_items_by_tagtagItems carrying the tag, filtered to the current page

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 resolves the field from the cached item (fetching the item from 1Password on demand when it is not cached), decrypts the value locally, and issues an internal browser_type call to the upstream server with the resolved value. Typing is refused unless the current page is in the item's URL set (the same host + path-prefix match as discovery), and refused when the current page URL cannot be determined.

Cache lifetime

Discovered items — with their field values encrypted — are cached in memory, write-once. Calling the upstream browser_close tool empties this cache (the close is still forwarded to the browser as usual); otherwise it lives for the process lifetime.

Workflow: find, then type

  1. Navigate to the login page, then call a discovery tool — e.g. browser_list_items — to obtain the vault and item IDs of an item usable on that page.
  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

OptionDefaultMeaning
--package-managerpnpmpnpm (pnpm dlx), npm (npx -y), or none (pre-installed)
--mcp-versionlatest@playwright/mcp version tag/range (ignored when none)
--mcp-binmcp-server-playwrightPre-installed binary; implies --package-manager none
--command(none)Explicit upstream command override
--op-commandop1Password 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
--version(none)Print the version and exit
-- <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.
  • Revealed items are cached in-memory for the process lifetime (or until the browser is closed via browser_close) in an obfuscated vault: each field value is 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