diff --git a/.devcontainer/claude/Dockerfile b/.devcontainer/claude/Dockerfile new file mode 100644 index 000000000..c4dd9b522 --- /dev/null +++ b/.devcontainer/claude/Dockerfile @@ -0,0 +1,168 @@ +FROM oven/bun:latest + +ARG TZ +ENV TZ="$TZ" + +ARG CLAUDE_CODE_VERSION=latest + +# Install basic development tools and iptables/ipset +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + build-essential \ + bash \ + curl \ + less \ + git \ + openssh-client \ + pkg-config \ + libssl-dev \ + procps \ + sudo \ + fzf \ + zsh \ + man-db \ + unzip \ + gnupg2 \ + gh \ + iptables \ + ipset \ + iproute2 \ + dnsutils \ + aggregate \ + jq \ + nano \ + vim \ + wget \ + tar \ + xz-utils \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install multiple .NET SDK versions (8/9/10) via Microsoft's official dotnet-install script. +# https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.sh +RUN curl -fsSL https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.sh -o /tmp/dotnet-install.sh \ + && chmod +x /tmp/dotnet-install.sh \ + && /tmp/dotnet-install.sh --channel 10.0 --install-dir /usr/share/dotnet \ + && /tmp/dotnet-install.sh --channel 9.0 --install-dir /usr/share/dotnet \ + && /tmp/dotnet-install.sh --channel 8.0 --install-dir /usr/share/dotnet \ + && rm -f /tmp/dotnet-install.sh + +# Install uv (latest) into /usr/local/bin +# https://astral.sh/uv +RUN curl -fsSL https://astral.sh/uv/install.sh -o /tmp/uv-install.sh \ + && UV_UNMANAGED_INSTALL=/usr/local/bin sh /tmp/uv-install.sh \ + && rm -f /tmp/uv-install.sh \ + && uv --version + +ARG USERNAME=bun +ENV HOME=/home/${USERNAME} + +# XDG Base Directory spec +ENV XDG_CONFIG_HOME=${HOME}/.config +ENV XDG_CONFIG_DIRS=/etc/xdg +ENV XDG_CACHE_HOME=${HOME}/.cache +ENV XDG_DATA_HOME=${HOME}/.local/share +ENV XDG_DATA_DIRS=/usr/local/share:/usr/share +ENV XDG_STATE_HOME=${HOME}/.local/state +ENV XDG_BIN_HOME=${HOME}/.local/bin + +# Give bun (and other tooling) a guaranteed user-writable temp directory. +# Bun resolves temp dir in this order: BUN_TMPDIR, TMPDIR, TEMP, TMP, then platform default (/tmp on Linux). +# Using /tmp is the most robust choice in devcontainers (works even if $HOME is remapped or mounted oddly). +ENV TMPDIR=/tmp +ENV TEMP=/tmp +ENV TMP=/tmp +ENV BUN_TMPDIR=/tmp + +ENV DOTNET_ROOT=/usr/share/dotnet +ENV DOTNET_MULTILEVEL_LOOKUP=0 + +# Rust toolchain locations (installed via rustup as the non-root user) +ENV CARGO_HOME=${HOME}/.cargo +ENV RUSTUP_HOME=${HOME}/.rustup + +# Ensure user-local binaries (uv python, etc.) are available. +ENV PATH=${DOTNET_ROOT}:${CARGO_HOME}/bin:${XDG_BIN_HOME}:${PATH} + +# Persist bash history. +RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \ + && mkdir /commandhistory \ + && touch /commandhistory/.bash_history \ + && chown -R $USERNAME /commandhistory + +# Set `DEVCONTAINER` environment variable to help with orientation +ENV DEVCONTAINER=true + +# Create workspace and config directories +RUN mkdir -p /workspace $HOME/.claude && \ + chown -R ${USERNAME}:${USERNAME} /workspace $HOME/.claude + +WORKDIR /workspace + +ARG GIT_DELTA_VERSION=0.18.2 +RUN ARCH=$(dpkg --print-architecture) && \ + wget "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \ + dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \ + rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" + +# Set up non-root user +USER bun + +# Configure git defaults for the dev user +COPY --chown=bun:bun config/.gitconfig $HOME/.gitconfig + +# Pre-install multiple Python versions for the dev user via uv +RUN uv python install 3.10 3.11 3.12 3.13 3.14 + +# Install latest stable Rust (via rustup) for the non-root user +ARG RUSTUP_TOOLCHAIN=stable +RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain ${RUSTUP_TOOLCHAIN} && \ + rustup component add rustfmt clippy + +# Set the default shell to zsh rather than sh +ENV SHELL=/bin/zsh + +# Set the default editor and visual +ENV EDITOR=nano +ENV VISUAL=nano + +# Install starship prompt (must be done as root for /usr/local/bin) +USER root +RUN curl -sS https://starship.rs/install.sh | sh -s -- --yes +USER bun + +# Copy starship config +RUN mkdir -p $HOME/.config/starship +COPY --chown=bun:bun config/starship/starship.toml $HOME/.config/starship/starship.toml + +# Configure zsh with starship +RUN echo 'source /usr/share/doc/fzf/examples/key-bindings.zsh' >> $HOME/.zshrc && \ + echo 'source /usr/share/doc/fzf/examples/completion.zsh' >> $HOME/.zshrc && \ + echo "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" >> $HOME/.zshrc && \ + echo 'export XDG_CONFIG_HOME="$HOME/.config"' >> $HOME/.zshrc && \ + echo 'export XDG_CONFIG_DIRS=/etc/xdg' >> $HOME/.zshrc && \ + echo 'export XDG_CACHE_HOME="$HOME/.cache"' >> $HOME/.zshrc && \ + echo 'export XDG_DATA_HOME="$HOME/.local/share"' >> $HOME/.zshrc && \ + echo 'export XDG_DATA_DIRS=/usr/local/share:/usr/share' >> $HOME/.zshrc && \ + echo 'export XDG_STATE_HOME="$HOME/.local/state"' >> $HOME/.zshrc && \ + echo 'export XDG_BIN_HOME="$HOME/.local/bin"' >> $HOME/.zshrc && \ + echo 'export PATH="$XDG_BIN_HOME:$PATH"' >> $HOME/.zshrc && \ + echo 'export STARSHIP_CONFIG=$HOME/.config/starship/starship.toml' >> $HOME/.zshrc && \ + echo 'eval "$(starship init zsh)"' >> $HOME/.zshrc + +# Install Claude globally and Playwright browsers as root +USER root +RUN bun install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} + +# Copy and set up firewall script +COPY config/init-firewall.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/init-firewall.sh + +# Grant bun user passwordless sudo (standard for devcontainers) +RUN echo "bun ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/bun && \ + chmod 0440 /etc/sudoers.d/bun + +# Fix ownership of bun user directories (cache dirs are volume-mounted in devcontainer.json) +RUN mkdir -p /home/bun/.cache/starship /home/bun/.bun/install/cache && \ + chown -R bun:bun /home/bun/.cache /home/bun/.bun + +USER bun diff --git a/.devcontainer/claude/devcontainer.json b/.devcontainer/claude/devcontainer.json new file mode 100644 index 000000000..aa3d1cd8c --- /dev/null +++ b/.devcontainer/claude/devcontainer.json @@ -0,0 +1,90 @@ +{ + "name": "Claude", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2.13.0": { + "moby": false + } + }, + "build": { + "context": "..", + "dockerfile": "Dockerfile", + "args": { + "TZ": "${localEnv:TZ:America/Los_Angeles}", + "CLAUDE_CODE_VERSION": "latest", + "GIT_DELTA_VERSION": "0.18.2", + "ZSH_IN_DOCKER_VERSION": "1.2.0" + } + }, + "runArgs": [ + "--cap-add=NET_ADMIN", + "--cap-add=NET_RAW", + "--tmpfs=/tmp:exec,mode=1777,size=1g", + "--tmpfs=/var/tmp:exec,mode=1777,size=512m" + ], + "customizations": { + "vscode": { + "extensions": [ + "anthropic.claude-code", + "ms-dotnettools.csdevkit", + "njpwerner.autodocstring", + "ms-python.python", + "charliermarsh.ruff", + "ms-toolsai.jupyter", + "rust-lang.rust-analyzer", + "mhutchie.git-graph", + "streetsidesoftware.code-spell-checker", + "tamasfe.even-better-toml", + "yzhang.markdown-all-in-one", + "shd101wyy.markdown-preview-enhanced", + "esbenp.prettier-vscode" + ], + "settings": { + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[markdown]": { + "editor.defaultFormatter": "yzhang.markdown-all-in-one" + }, + "[plaintext]": { + "editor.formatOnPaste": false, + "editor.suggestSelection": "recentlyUsedByPrefix", + "editor.wordBasedSuggestions": "off" + }, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.defaultFoldingRangeProvider": "charliermarsh.ruff" + }, + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" + }, + "[toml]": { + "editor.defaultFormatter": "tamasfe.even-better-toml" + }, + "editor.formatOnSave": true + } + } + }, + "remoteUser": "bun", + "mounts": [ + "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume", + "source=claude-code-config-${devcontainerId},target=/home/node/.claude,type=volume", + "source=claude-code-bun-cache-${devcontainerId},target=/home/bun/.bun/install/cache,type=volume" + ], + "containerEnv": { + "CLAUDE_CONFIG_DIR": "/home/bun/.claude", + "TMPDIR": "/tmp", + "TEMP": "/tmp", + "TMP": "/tmp", + "BUN_INSTALL_CACHE_DIR": "/home/bun/.bun/install/cache" + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", + "workspaceFolder": "/workspace", + "postStartCommand": "sudo /usr/local/bin/init-firewall.sh", + "waitFor": "postStartCommand" +} diff --git a/.devcontainer/config/.gitconfig b/.devcontainer/config/.gitconfig new file mode 100644 index 000000000..17550e59e --- /dev/null +++ b/.devcontainer/config/.gitconfig @@ -0,0 +1,11 @@ +[alias] + rf = "reflog" + s = "status -s" + sv = "status" + p = "push" + pl = "pull" + ac = "!git add . && git commit -m" + c = "commit -m" + gg = "!git fetch origin && git reset --hard @{u} && git clean -fd" +[push] + autoSetupRemote = true \ No newline at end of file diff --git a/.devcontainer/config/init-firewall.sh b/.devcontainer/config/init-firewall.sh new file mode 100755 index 000000000..20569b90c --- /dev/null +++ b/.devcontainer/config/init-firewall.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +set -euo pipefail # Exit on error, undefined vars, and pipeline failures +IFS=$'\n\t' # Stricter word splitting + +# 1. Extract Docker DNS info BEFORE any flushing +DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true) + +# Flush existing rules and delete existing ipsets +iptables -F +iptables -X +iptables -t nat -F +iptables -t nat -X +iptables -t mangle -F +iptables -t mangle -X +ipset destroy allowed-domains 2>/dev/null || true + +# 2. Selectively restore ONLY internal Docker DNS resolution +if [ -n "$DOCKER_DNS_RULES" ]; then + echo "Restoring Docker DNS rules..." + iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true + iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true + echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat +else + echo "No Docker DNS rules to restore" +fi + +# First allow DNS and localhost before any restrictions +# Allow outbound DNS +iptables -A OUTPUT -p udp --dport 53 -j ACCEPT +# Allow inbound DNS responses +iptables -A INPUT -p udp --sport 53 -j ACCEPT +# Allow outbound SSH +iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT +# Allow inbound SSH responses +iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT +# Allow localhost +iptables -A INPUT -i lo -j ACCEPT +iptables -A OUTPUT -o lo -j ACCEPT + +# Create ipset with CIDR support +ipset create allowed-domains hash:net + +# Fetch GitHub meta information and aggregate + add their IP ranges +echo "Fetching GitHub IP ranges..." +gh_ranges=$(curl -s https://api.github.com/meta) +if [ -z "$gh_ranges" ]; then + echo "ERROR: Failed to fetch GitHub IP ranges" + exit 1 +fi + +if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then + echo "ERROR: GitHub API response missing required fields" + exit 1 +fi + +echo "Processing GitHub IPs..." +while read -r cidr; do + if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then + echo "ERROR: Invalid CIDR range from GitHub meta: $cidr" + exit 1 + fi + echo "Adding GitHub range $cidr" + ipset add allowed-domains "$cidr" +done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q) + +# Resolve and add other allowed domains +for domain in \ + "mcr.microsoft.com" \ + "mcp.deepwiki.com" \ + "registry.npmjs.org" \ + "api.anthropic.com" \ + "sentry.io" \ + "statsig.anthropic.com" \ + "statsig.com" \ + "marketplace.visualstudio.com" \ + "vscode.blob.core.windows.net" \ + "update.code.visualstudio.com" \ + "stackoverflow.com" \ + "dev.to" \ + "medium.com" \ + "hashnode.com" \ + "css-tricks.com" \ + "smashingmagazine.com" \ + "devdocs.io" \ + "docs.docker.com" \ + "docs.python.org" \ + "pypi.org" \ + "fastapi.tiangolo.com" \ + "flask.palletsprojects.com" \ + "www.djangoproject.com" \ + "nodejs.org" \ + "docs.npmjs.com" \ + "www.typescriptlang.org" \ + "developer.mozilla.org" \ + "react.dev" \ + "vuejs.org" \ + "angular.io" \ + "svelte.dev" \ + "nextjs.org" \ + "go.dev" \ + "pkg.go.dev" \ + "www.rust-lang.org" \ + "doc.rust-lang.org" \ + "docs.rs" \ + "crates.io" \ + "www.ruby-lang.org" \ + "rubygems.org" \ + "learn.microsoft.com" \ + "web.dev"; do + echo "Resolving $domain..." + ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}') + if [ -z "$ips" ]; then + echo "ERROR: Failed to resolve $domain" + exit 1 + fi + + while read -r ip; do + if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + echo "ERROR: Invalid IP from DNS for $domain: $ip" + exit 1 + fi + echo "Adding $ip for $domain" + ipset add allowed-domains "$ip" 2>/dev/null || true + done < <(echo "$ips") +done + +# Get host IP from default route +HOST_IP=$(ip route | grep default | cut -d" " -f3) +if [ -z "$HOST_IP" ]; then + echo "ERROR: Failed to detect host IP" + exit 1 +fi + +HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/") +echo "Host network detected as: $HOST_NETWORK" + +# Set up remaining iptables rules +iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT +iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT + +# Set default policies to DROP first +iptables -P INPUT DROP +iptables -P FORWARD DROP +iptables -P OUTPUT DROP + +# First allow established connections for already approved traffic +iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT +iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + +# Then allow only specific outbound traffic to allowed domains +iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT + +# Explicitly REJECT all other outbound traffic for immediate feedback +iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited + +echo "Firewall configuration complete" +echo "Verifying firewall rules..." +if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then + echo "ERROR: Firewall verification failed - was able to reach https://example.com" + exit 1 +else + echo "Firewall verification passed - unable to reach https://example.com as expected" +fi + +# Verify GitHub API access +if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then + echo "ERROR: Firewall verification failed - unable to reach https://api.github.com" + exit 1 +else + echo "Firewall verification passed - able to reach https://api.github.com as expected" +fi \ No newline at end of file diff --git a/.devcontainer/config/starship/starship.toml b/.devcontainer/config/starship/starship.toml new file mode 100644 index 000000000..0e3c68cc8 --- /dev/null +++ b/.devcontainer/config/starship/starship.toml @@ -0,0 +1,228 @@ +# Get editor completions based on the config schema +"$schema" = 'https://starship.rs/config-schema.json' + +format = """ +$shlvl\ +$singularity\ +$kubernetes\ +$directory\ +$vcsh\ +$fossil_branch\ +$fossil_metrics\ +$git_branch\ +$git_commit\ +$git_state\ +$git_metrics\ +$git_status\ +$hg_branch\ +$pijul_channel\ +$docker_context\ +$package\ +$c\ +$cmake\ +$cobol\ +$daml\ +$dart\ +$deno\ +$dotnet\ +$elixir\ +$elm\ +$erlang\ +$fennel\ +$gleam\ +$golang\ +$guix_shell\ +$haskell\ +$haxe\ +$helm\ +$java\ +$julia\ +$kotlin\ +$gradle\ +$lua\ +$nim\ +$nodejs\ +$ocaml\ +$opa\ +$perl\ +$php\ +$pulumi\ +$purescript\ +$python\ +$quarto\ +$raku\ +$rlang\ +$red\ +$ruby\ +$rust\ +$scala\ +$solidity\ +$swift\ +$terraform\ +$typst\ +$vlang\ +$vagrant\ +$zig\ +$buf\ +$nix_shell\ +$meson\ +$spack\ +$memory_usage\ +$openstack\ +$nats\ +$direnv\ +$env_var\ +$crystal\ +$custom\ +$sudo\ +$line_break\ +$jobs\ +$time\ +$status\ +$os\ +$container\ +$shell\ +$character""" + +right_format = """$cmd_duration""" + +# Sets user-defined palette +# Palettes must be defined _after_ this line +palette = "catppuccin_mocha" + +# Disable the blank line at the start of the prompt +add_newline = false + +[line_break] +disabled = false + +# Starship modules +[character] +# Note the use of Catppuccin color 'peach' +success_symbol = "[❯](green)" +error_symbol = "[❯](red)" +vimcmd_symbol = "[❮](subtext1)" # For use with zsh-vi-mode + +[gcloud] +format = '[$symbol$active]($style) ' +style = 'bold yellow' + +[git_branch] +style = "bold mauve" + +[directory] +truncation_length = 4 +style = "bold lavender" + +# Palette definitions +[palettes.catppuccin_latte] +rosewater = "#dc8a78" +flamingo = "#dd7878" +pink = "#ea76cb" +mauve = "#8839ef" +red = "#d20f39" +maroon = "#e64553" +peach = "#fe640b" +yellow = "#df8e1d" +green = "#40a02b" +teal = "#179299" +sky = "#04a5e5" +sapphire = "#209fb5" +blue = "#1e66f5" +lavender = "#7287fd" +text = "#4c4f69" +subtext1 = "#5c5f77" +subtext0 = "#6c6f85" +overlay2 = "#7c7f93" +overlay1 = "#8c8fa1" +overlay0 = "#9ca0b0" +surface2 = "#acb0be" +surface1 = "#bcc0cc" +surface0 = "#ccd0da" +base = "#eff1f5" +mantle = "#e6e9ef" +crust = "#dce0e8" + +[palettes.catppuccin_frappe] +rosewater = "#f2d5cf" +flamingo = "#eebebe" +pink = "#f4b8e4" +mauve = "#ca9ee6" +red = "#e78284" +maroon = "#ea999c" +peach = "#ef9f76" +yellow = "#e5c890" +green = "#a6d189" +teal = "#81c8be" +sky = "#99d1db" +sapphire = "#85c1dc" +blue = "#8caaee" +lavender = "#babbf1" +text = "#c6d0f5" +subtext1 = "#b5bfe2" +subtext0 = "#a5adce" +overlay2 = "#949cbb" +overlay1 = "#838ba7" +overlay0 = "#737994" +surface2 = "#626880" +surface1 = "#51576d" +surface0 = "#414559" +base = "#303446" +mantle = "#292c3c" +crust = "#232634" + +[palettes.catppuccin_macchiato] +rosewater = "#f4dbd6" +flamingo = "#f0c6c6" +pink = "#f5bde6" +mauve = "#c6a0f6" +red = "#ed8796" +maroon = "#ee99a0" +peach = "#f5a97f" +yellow = "#eed49f" +green = "#a6da95" +teal = "#8bd5ca" +sky = "#91d7e3" +sapphire = "#7dc4e4" +blue = "#8aadf4" +lavender = "#b7bdf8" +text = "#cad3f5" +subtext1 = "#b8c0e0" +subtext0 = "#a5adcb" +overlay2 = "#939ab7" +overlay1 = "#8087a2" +overlay0 = "#6e738d" +surface2 = "#5b6078" +surface1 = "#494d64" +surface0 = "#363a4f" +base = "#24273a" +mantle = "#1e2030" +crust = "#181926" + +[palettes.catppuccin_mocha] +rosewater = "#f5e0dc" +flamingo = "#f2cdcd" +pink = "#f5c2e7" +mauve = "#cba6f7" +red = "#f38ba8" +maroon = "#eba0ac" +peach = "#fab387" +yellow = "#f9e2af" +green = "#a6e3a1" +teal = "#94e2d5" +sky = "#89dceb" +sapphire = "#74c7ec" +blue = "#89b4fa" +lavender = "#b4befe" +text = "#cdd6f4" +subtext1 = "#bac2de" +subtext0 = "#a6adc8" +overlay2 = "#9399b2" +overlay1 = "#7f849c" +overlay0 = "#6c7086" +surface2 = "#585b70" +surface1 = "#45475a" +surface0 = "#313244" +base = "#1e1e2e" +mantle = "#181825" +crust = "#11111b" diff --git a/.mcp.json b/.mcp.json index 00b2fd1ea..ba14c2fb9 100644 --- a/.mcp.json +++ b/.mcp.json @@ -5,8 +5,15 @@ "url": "https://mcp.deepwiki.com/mcp" }, "playwright": { - "command": "bunx", - "args": ["@playwright/mcp@latest", "--headless"] + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "--init", + "--pull=always", + "mcr.microsoft.com/playwright/mcp" + ] } } } diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 2b879d385..754a80945 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -5,8 +5,16 @@ "url": "https://mcp.deepwiki.com/mcp" }, "playwright": { - "command": "bunx", - "args": ["@playwright/mcp@latest", "--headless"] + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "--init", + "--pull=always", + "--network=host", + "mcr.microsoft.com/playwright/mcp" + ] } } }