This repository has been archived on 2026-09-17. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
nixos-config/home/tnix/localModules/playwright-mcp.nix
pijukka 5259fe44ee home(builder): give codex and claude playwright mcp by default
Both agents now get the MCP server without a separate launcher: home-manager
owns one config file per agent next to each agent's own mutable state file
(~/.codex/config.toml and ~/.claude.json stay untouched), and zsh aliases make
the layered profile / --mcp-config call the default.

Drop pkgs.chromium and --executable-path: the browsers bundled with
playwright-mcp work on their own, so this removes a redundant chromium closure.
--browser chrome would have looked for a branded Chrome under /opt/google.
Add --output-dir so page snapshots stop landing in whatever repository the
agent runs in, and give each agent its own profile so both can drive a browser
in parallel.

Verified on tnix: codex --profile playwright mcp list shows the server, and
claude reports browser_navigate as available (NO in a control directory).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01543pvEuBqu1G4rimyXq3YA
2026-09-17 11:14:22 +02:00

84 lines
3.4 KiB
Nix

{ config, lib, pkgs, ... }:
let
# This is intentionally an explicit guard in addition to flake.lock. An
# input update must consciously update this value and repeat the MCP smoke
# test documented in docs/playwright-mcp.md.
playwrightMcpVersion = "0.0.76";
stateDir = "${config.home.homeDirectory}/.local/share/playwright-mcp";
# A Chromium user-data directory may be used by exactly one process at a
# time. Each agent therefore gets its own profile, so Codex and Claude may
# drive a browser in parallel instead of failing over a locked profile.
agents = [ "codex" "claude" ];
profileDir = agent: "${stateDir}/${agent}/profile";
# The browsers Playwright was tested against already ship with the
# playwright-mcp package; pkgs.chromium would be a second, redundant browser
# closure. "chrome" would instead look for a branded Google Chrome under
# /opt/google/chrome and fail.
mkServer = agent: {
command = "${pkgs.playwright-mcp}/bin/playwright-mcp";
args = [
"--browser"
"chromium"
"--user-data-dir"
(profileDir agent)
# Without this the server drops page snapshots into a .playwright-mcp
# directory below the current working directory, i.e. into whatever
# repository the agent happens to run in.
"--output-dir"
"${stateDir}/${agent}/output"
];
# The nixpkgs wrapper switches to an in-memory profile unless this is set,
# which would discard logins between sessions.
env.PLAYWRIGHT_MCP_USER_DATA_DIR = profileDir agent;
};
codexConfig = (pkgs.formats.toml { }).generate "codex-playwright.toml" {
mcp_servers.playwright = mkServer "codex" // {
startup_timeout_sec = 20;
tool_timeout_sec = 60;
};
};
claudeConfig = (pkgs.formats.json { }).generate "claude-playwright.json" {
mcpServers.playwright = mkServer "claude";
};
claudeConfigPath = "${config.home.homeDirectory}/.claude/mcp-playwright.json";
in
{
assertions = [
{
assertion = pkgs.playwright-mcp.version == playwrightMcpVersion;
message = "playwright-mcp changed from ${playwrightMcpVersion}; review its release notes, update the pin, and repeat the MCP smoke test.";
}
];
# Both agents read a file this module owns. Neither agent's own mutable
# state file is touched: Codex rewrites ~/.codex/config.toml (trust levels,
# model notices) and Claude rewrites ~/.claude.json, so home-manager must
# not own either. Codex layers a named profile on top of its base config,
# Claude takes an additional config file on the command line.
home.file.".codex/playwright.config.toml".source = codexConfig;
home.file.".claude/mcp-playwright.json".source = claudeConfig;
# Make it the default for the interactive shell rather than a separate
# command that has to be remembered. zsh does not re-expand an alias into
# itself, so these still call the real binaries.
programs.zsh.shellAliases = {
codex = "codex --profile playwright";
claude = "claude --mcp-config ${claudeConfigPath}";
};
# Browser profiles are runtime data, not Nix store artifacts; they only need
# to exist with private permissions before the first launch.
home.activation.playwrightMcpDirs =
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
run ${pkgs.coreutils}/bin/install -d -m 0700 ${lib.escapeShellArg stateDir} \
${lib.concatMapStringsSep " " (agent:
lib.escapeShellArg "${stateDir}/${agent}") agents}
'';
}