playwright-secure-mcp
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/mcpas 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), andbrowser_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 inbrowser_type),vault(1Password vault ID),item(1Password item ID),field(e.g.usernameorpassword) - Optional:
submit,slowly(as inbrowser_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
- Call a discovery tool to obtain the
vaultanditemIDs — e.g. pass the login page URL tobrowser_find_secret_by_url. - Call
browser_type_secretwith those IDs and thefieldto 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 pnpmornpmto download@playwright/mcpon 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:
- 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.
- TPM 2.0 (Linux, hardware): the data key is sealed inside the platform TPM via the tpm2-tss ESYS library over
/dev/tpmrm0and unsealed transiently per crypto batch, then zeroed. The binary links tpm2-tss, which is assumed present on the host. - 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.
- 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
playwright-secure-mcp
- 0
- 0
- 0
- 0
- 2
- about 6 hours ago
- July 13, 2026
MIT License
Thu, 16 Jul 2026 00:34:32 GMT