Files
server-deploy/claude-dev-stack/deploy.ps1

897 lines
43 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#Requires -Version 5.1
<#
.SYNOPSIS
WSL2 + Claude Code CLI + Unity MCP + Rust Token Killer 全栈一键部署脚本
.DESCRIPTION
自动完成以下步骤:
0. 检测 v2rayN 代理端口,配置 Windows & WSL2 两侧代理
1. 启用 WSL2 功能 & 安装 Ubuntu 发行版
2. 检测/安装 Windows 本机 Node.jsAI 客户端 MCP 需要)
3. WSL2 内安装 Node.js LTSClaude Code CLI 需要)
4. 安装 Claude Code CLI (@anthropic-ai/claude-code)
5. 安装 Unity MCP ServerWindows + WSL2 双侧)& 写入各 AI 客户端配置
6. 配置 Windows 防火墙放行 MCP Bridge 端口
7. 安装 Rust 工具链 & Token Killer (rtk)
8. 写入 PowerShell Profile 快捷命令
.PARAMETER ProxyPort
v2rayN HTTP 代理端口0 = 自动检测;-1 = 跳过代理
.PARAMETER BridgePort
Unity MCP Bridge 端口,默认 7890
.PARAMETER InstallDir
Windows 侧 unity-mcp-server 安装目录
.PARAMETER UnityHubPath
Unity Hub 路径(写入 MCP 配置 env
.PARAMETER SkipFirewall
跳过防火墙规则配置
.PARAMETER SkipWSL
跳过 WSL2 安装步骤
.NOTES
首次安装 WSL2 需以管理员身份运行。
已有 WSL2 可普通终端运行,加 -SkipWSL 跳过 WSL2 安装检查。
#>
param(
[int] $ProxyPort = 0,
[int] $BridgePort = 7890,
[string]$InstallDir = "$env:USERPROFILE\unity-mcp-server",
[string]$UnityHubPath = "C:\Program Files\Unity Hub\Unity Hub.exe",
[switch]$SkipFirewall,
[switch]$SkipWSL
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# ──────────────────────────────────────────────────────────────
# 颜色日志辅助
# ──────────────────────────────────────────────────────────────
function Write-Step { param($msg) Write-Host "`n==== $msg ====" -ForegroundColor Cyan }
function Write-OK { param($msg) Write-Host " [OK] $msg" -ForegroundColor Green }
function Write-Info { param($msg) Write-Host " [..] $msg" -ForegroundColor DarkGray }
function Write-Warn { param($msg) Write-Host " [!!] $msg" -ForegroundColor Yellow }
function Write-Fail { param($msg) Write-Host " [ERR] $msg" -ForegroundColor Red }
# ──────────────────────────────────────────────────────────────
# 加载 .env 配置
# ──────────────────────────────────────────────────────────────
$ScriptDir = $PSScriptRoot
$EnvFile = Join-Path $ScriptDir ".env"
$EnvExample = Join-Path $ScriptDir ".env.example"
if (-not (Test-Path $EnvFile)) {
if (Test-Path $EnvExample) {
Copy-Item $EnvExample $EnvFile
Write-Warn "已从 .env.example 创建 .env请按需编辑后重新运行"
Write-Warn " notepad $EnvFile"
exit 0
}
}
$Config = @{}
if (Test-Path $EnvFile) {
Get-Content $EnvFile | Where-Object { $_ -match "^\s*[^#].*=" } | ForEach-Object {
$parts = $_ -split "=", 2
$key = $parts[0].Trim()
$val = $parts[1].Trim().Trim('"').Trim("'")
$Config[$key] = $val
}
Write-Info "已加载配置:$EnvFile"
}
$ANTHROPIC_API_KEY = if ($Config["ANTHROPIC_API_KEY"]) { $Config["ANTHROPIC_API_KEY"] } else { "" }
$ANTHROPIC_AUTH_TOKEN = if ($Config["ANTHROPIC_AUTH_TOKEN"]) { $Config["ANTHROPIC_AUTH_TOKEN"] } else { "" }
$ANTHROPIC_BASE_URL = if ($Config["ANTHROPIC_BASE_URL"]) { $Config["ANTHROPIC_BASE_URL"] } else { "https://api.lmuai.com" }
$CLAUDE_MODEL = if ($Config["CLAUDE_MODEL"]) { $Config["CLAUDE_MODEL"] } else { "claude-sonnet-4-6" }
$WSL_DISTRO = if ($Config["WSL_DISTRO"]) { $Config["WSL_DISTRO"] } else { "Ubuntu" }
$SKIP_WSL_INSTALL = if ($Config["SKIP_WSL_INSTALL"]) { $Config["SKIP_WSL_INSTALL"] } else { "false" }
# 判断认证方式:灵眸/中转用 AUTH_TOKEN官方用 API_KEY
$UseLmuAuth = ($ANTHROPIC_AUTH_TOKEN -ne "" -and $ANTHROPIC_BASE_URL -ne "https://api.anthropic.com")
# ──────────────────────────────────────────────────────────────
# Banner
# ──────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║ WSL2 + Claude Code CLI + Unity MCP + RTK 全栈部署 ║" -ForegroundColor Cyan
Write-Host "║ AnkleBreaker Unity MCP · v2rayN 代理自动检测 ║" -ForegroundColor Cyan
Write-Host "╚══════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
# ══════════════════════════════════════════════════════════════
# Step 0: v2rayN 代理检测与配置
# ══════════════════════════════════════════════════════════════
Write-Step "0/8 v2rayN 代理检测"
# ── 检测代理端口 ──────────────────────────────────────────────
function Get-V2RayNProxyPort {
# 1. Windows 系统代理注册表v2rayN"设为系统代理"时写入)
try {
$ie = Get-ItemProperty `
"HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings" `
-ErrorAction SilentlyContinue
if ($ie.ProxyEnable -eq 1 -and $ie.ProxyServer -match ":(\d+)") {
return [int]$Matches[1]
}
} catch {}
# 2. v2rayN 配置文件
$cfgPaths = @(
"$env:APPDATA\v2rayN\guiNConfig.json",
"$env:LOCALAPPDATA\v2rayN\guiNConfig.json"
)
foreach ($p in $cfgPaths) {
if (Test-Path $p) {
try {
$j = Get-Content $p -Raw | ConvertFrom-Json
if ($j.localPort) { return [int]$j.localPort }
if ($j.httpPort) { return [int]$j.httpPort }
} catch {}
}
}
# 3. 探测常用端口
foreach ($port in @(10809, 10808, 7890, 1080, 8080)) {
try {
$tcp = New-Object System.Net.Sockets.TcpClient
$ar = $tcp.BeginConnect("127.0.0.1", $port, $null, $null)
if ($ar.AsyncWaitHandle.WaitOne(300, $false)) {
$tcp.EndConnect($ar); $tcp.Close(); return $port
}
$tcp.Close()
} catch {}
}
return 0
}
function Set-WindowsProxy {
param([int]$Port)
$url = "http://127.0.0.1:$Port"
git config --global http.proxy $url 2>$null
git config --global https.proxy $url 2>$null
& npm config set proxy $url --location global 2>$null
& npm config set https-proxy $url --location global 2>$null
$env:http_proxy = $url; $env:https_proxy = $url
$env:HTTP_PROXY = $url; $env:HTTPS_PROXY = $url
Write-OK "Windows git/npm/env 代理 -> $url"
}
function Clear-WindowsProxy {
git config --global --unset http.proxy 2>$null
git config --global --unset https.proxy 2>$null
& npm config delete proxy --location global 2>$null
& npm config delete https-proxy --location global 2>$null
"http_proxy","https_proxy","HTTP_PROXY","HTTPS_PROXY" |
ForEach-Object { Remove-Item "Env:\$_" -ErrorAction SilentlyContinue }
}
$resolvedProxyPort = 0
# 灵眸 AI 国内直连无需代理,主动跳过
if ($UseLmuAuth -and $ProxyPort -ne -1) {
Write-Info "灵眸 AI 模式:国内直连,自动跳过代理配置"
Write-Info "如需为其他服务git/npm使用代理请手动指定 -ProxyPort"
$ProxyPort = -1
}
if ($ProxyPort -eq -1) {
Write-Warn "已跳过代理配置(-ProxyPort -1"
} elseif ($ProxyPort -gt 0) {
$resolvedProxyPort = $ProxyPort
Set-WindowsProxy -Port $resolvedProxyPort
Write-OK "使用指定代理端口: $resolvedProxyPort"
} else {
$resolvedProxyPort = Get-V2RayNProxyPort
if ($resolvedProxyPort -gt 0) {
Set-WindowsProxy -Port $resolvedProxyPort
Write-OK "自动检测到 v2rayN 代理端口: $resolvedProxyPort"
} else {
Write-Warn "未检测到活跃代理,将使用直连网络"
Write-Warn "如需代理,请指定: pwsh deploy.ps1 -ProxyPort 10809"
Write-Warn "⚠ 确保 v2rayN 已开启「允许来自局域网的连接」WSL2 需要)"
}
}
# ══════════════════════════════════════════════════════════════
# Step 1: WSL2 安装
# ══════════════════════════════════════════════════════════════
Write-Step "1/8 WSL2 环境检测 & 安装"
$distroInstalled = $false
try {
$dl = wsl --list --quiet 2>&1 | ForEach-Object { $_ -replace "`0","" }
if ($dl -match [regex]::Escape($WSL_DISTRO)) { $distroInstalled = $true }
} catch {}
if ($distroInstalled) {
Write-OK "WSL2 + $WSL_DISTRO 已安装,跳过"
} elseif ($SkipWSL -or $SKIP_WSL_INSTALL -eq "true") {
Write-Warn "跳过 WSL2 安装"
} else {
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Fail "安装 WSL2 需要管理员权限,请以管理员身份运行 PowerShell"
Write-Info "如已安装 WSL2请加 -SkipWSL 参数或在 .env 设置 SKIP_WSL_INSTALL=true"
exit 1
}
$f1 = Get-WindowsOptionalFeature -Online -FeatureName "Microsoft-Windows-Subsystem-Linux" -ErrorAction SilentlyContinue
if ($f1.State -ne "Enabled") {
Enable-WindowsOptionalFeature -Online -FeatureName "Microsoft-Windows-Subsystem-Linux" -NoRestart | Out-Null
}
$f2 = Get-WindowsOptionalFeature -Online -FeatureName "VirtualMachinePlatform" -ErrorAction SilentlyContinue
if ($f2.State -ne "Enabled") {
Enable-WindowsOptionalFeature -Online -FeatureName "VirtualMachinePlatform" -NoRestart | Out-Null
}
wsl --update 2>&1 | Out-Null
wsl --set-default-version 2 2>&1 | Out-Null
wsl --install -d $WSL_DISTRO --no-launch 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Fail "WSL2 安装失败,系统可能需要重启后重新运行"
exit 1
}
Write-OK "WSL2 $WSL_DISTRO 安装完成"
Write-Warn "首次安装可能需要重启,重启后重新运行脚本"
}
# ── WSL2 执行辅助函数 ─────────────────────────────────────────
function Invoke-WSL {
param([string]$Command, [switch]$IgnoreError)
if ($Command -match "`n") {
# 多行命令base64 编码后在 bash 内解码执行,完全规避 PowerShell 管道 CRLF 问题
$CleanCmd = $Command -replace "`r`n","`n" -replace "`r","`n"
$b64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($CleanCmd))
$result = wsl -d $WSL_DISTRO -- bash -c "echo '$b64' | base64 -d | bash" 2>&1 |
ForEach-Object { ($_ -replace "`0","").ToString() }
} else {
$result = wsl -d $WSL_DISTRO -- bash -c $Command 2>&1 |
ForEach-Object { ($_ -replace "`0","").ToString() }
}
if ($LASTEXITCODE -ne 0 -and -not $IgnoreError) {
Write-Fail "WSL 命令失败 (exit $LASTEXITCODE)"
Write-Fail ($result -join "`n")
exit 1
}
return ($result -join "`n")
}
# 用 exit code 方式测试连通性,避免输出编码干扰
wsl -d $WSL_DISTRO -- bash -c "exit 0" 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Fail "无法访问 WSL2 发行版 '$WSL_DISTRO'"
exit 1
}
Write-OK "WSL2 ($WSL_DISTRO) 连接正常"
# 获取 WSL2 默认登录用户OOBE 完成后可能是非 root
$wslUser = (wsl -d $WSL_DISTRO -- bash -c "whoami" 2>&1) -replace "`0","" |
Where-Object { $_ -match "^\w+$" } | Select-Object -First 1
if (-not $wslUser) { $wslUser = "root" }
$wslUser = $wslUser.Trim()
Write-OK "WSL2 默认用户: $wslUser"
if ($wslUser -ne "root") {
Write-Info "非 root 用户,系统级命令将通过 sudo 执行"
}
# ── Step 1a: 配置 .wslconfigmirrored 网络模式) ────────────
$wslCfgPath = "$env:USERPROFILE\.wslconfig"
$wslCfgContent = @"
[wsl2]
networkingMode=mirrored
dnsTunneling=true
firewall=true
autoProxy=true
"@
$needRestart = $false
if (Test-Path $wslCfgPath) {
$existing = Get-Content $wslCfgPath -Raw
if ($existing -notmatch "networkingMode=mirrored") {
Set-Content $wslCfgPath $wslCfgContent -Encoding UTF8
$needRestart = $true
Write-OK ".wslconfig 已更新 -> networkingMode=mirrored"
}
} else {
Set-Content $wslCfgPath $wslCfgContent -Encoding UTF8
$needRestart = $true
Write-OK ".wslconfig 已创建 -> networkingMode=mirrored"
}
if ($needRestart) {
Write-Info "重启 WSL2 以应用镜像网络模式..."
wsl --shutdown 2>$null
Start-Sleep 3
}
# ── 配置 WSL2 侧代理(持久化到 ~/.bashrc ───────────────────
if ($resolvedProxyPort -gt 0) {
Write-Info "配置 WSL2 代理(端口 $resolvedProxyPort..."
# mirrored 模式下 127.0.0.1 即 Windows直接用即可
$proxyEnvContent = "# WSL2 mirrored networking: 127.0.0.1 = Windows host`nexport http_proxy=`"http://127.0.0.1:$resolvedProxyPort`"`nexport https_proxy=`"http://127.0.0.1:$resolvedProxyPort`"`nexport HTTP_PROXY=`"http://127.0.0.1:$resolvedProxyPort`"`nexport HTTPS_PROXY=`"http://127.0.0.1:$resolvedProxyPort`"`nexport no_proxy=`"localhost,127.0.0.1,::1`"`n"
$tmpFile = "$env:TEMP\mcp-proxy.env"
$bytes = [System.Text.Encoding]::UTF8.GetBytes($proxyEnvContent)
[System.IO.File]::WriteAllBytes($tmpFile, $bytes)
Copy-Item $tmpFile "\\wsl.localhost\$WSL_DISTRO\tmp\mcp-proxy-deploy.env" -Force 2>$null
Invoke-WSL "cp /tmp/mcp-proxy-deploy.env ~/.mcp-proxy.env && chmod 644 ~/.mcp-proxy.env && echo OK" | Out-Null
# 写入 ~/.bashrc幂等
Invoke-WSL @'
if ! grep -q 'mcp-proxy.env' ~/.bashrc 2>/dev/null; then
printf '\n# Unity MCP WSL2 proxy (generated by deploy.ps1)\n[ -f ~/.mcp-proxy.env ] && . ~/.mcp-proxy.env\n' >> ~/.bashrc
fi
# 立即应用代理并配置 git
. ~/.mcp-proxy.env 2>/dev/null || true
if [ -n "$http_proxy" ]; then
git config --global http.proxy "$http_proxy"
git config --global https.proxy "$https_proxy"
echo "WSL2 git proxy set: $http_proxy"
fi
true
'@ -IgnoreError | Out-Null
Write-OK "WSL2 代理已写入 ~/.mcp-proxy.env + ~/.bashrc"
Write-Warn "⚠ 请确保 v2rayN 已开启「允许来自局域网的连接」!"
}
# ══════════════════════════════════════════════════════════════
# Step 2: Windows 本机 Node.js 检查AI 客户端 MCP 需要)
# ══════════════════════════════════════════════════════════════
Write-Step "2/8 Windows Node.js 检查"
$winNode = Get-Command node -ErrorAction SilentlyContinue
if ($winNode) {
$winNodeVer = & node --version
Write-OK "Windows Node.js 已安装: $winNodeVer"
} else {
Write-Warn "Windows 本机未检测到 Node.js"
Write-Info "尝试通过 winget 安装 Node.js LTS..."
$wingetCmd = Get-Command winget -ErrorAction SilentlyContinue
if ($wingetCmd) {
winget install --id OpenJS.NodeJS.LTS --silent --accept-source-agreements --accept-package-agreements 2>&1 | Out-Null
# 刷新 PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("Path","User")
$winNode = Get-Command node -ErrorAction SilentlyContinue
if ($winNode) {
Write-OK "Node.js 安装成功: $(& node --version)"
} else {
Write-Warn "winget 安装后未找到 node请手动安装 Node.js 18+"
Write-Warn " https://nodejs.org/zh-cn/download"
Write-Warn " 或: winget install OpenJS.NodeJS.LTS"
}
} else {
Write-Warn "未找到 winget请手动安装 Node.js 18+"
Write-Warn " https://nodejs.org/zh-cn/download"
}
}
# 对 Windows npm 设置代理
if ($resolvedProxyPort -gt 0 -and (Get-Command npm -ErrorAction SilentlyContinue)) {
$proxyUrl = "http://127.0.0.1:$resolvedProxyPort"
& npm config set proxy $proxyUrl --location global 2>$null
& npm config set https-proxy $proxyUrl --location global 2>$null
}
# ══════════════════════════════════════════════════════════════
# Step 3: WSL2 系统依赖 & Node.js
# ══════════════════════════════════════════════════════════════
Write-Step "3/8 WSL2 Node.js LTS"
# 检测 WSL2 原生 node排除通过 WSL interop 调用的 Windows node路径含 /mnt/
$nodeVer = Invoke-WSL @'
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
_n=$(which node 2>/dev/null)
if [ -n "$_n" ] && ! echo "$_n" | grep -q '/mnt/'; then
node --version
else
echo MISSING
fi
true
'@ -IgnoreError
if ($nodeVer -match "v\d+") {
Write-OK "WSL2 Node.js 已安装 (原生): $($nodeVer.Trim())"
} else {
Write-Info "安装 WSL2 Node.js LTS (via nvm)..."
$installNodeCmd = @'
. ~/.mcp-proxy.env 2>/dev/null || true
export DEBIAN_FRONTEND=noninteractive
sudo apt-get install -y -qq curl ca-certificates 2>/dev/null
# 安装 nvm
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
fi
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
# 安装 Node.js LTS
nvm install --lts
nvm use --lts
# 写入 .bashrc幂等
grep -q 'NVM_DIR' ~/.bashrc || cat >> ~/.bashrc << 'NVMEOF'
# nvm (Node Version Manager)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
NVMEOF
node --version
npm --version
true
'@
Invoke-WSL $installNodeCmd
Write-OK "WSL2 Node.js 安装完成"
}
# ══════════════════════════════════════════════════════════════
# Step 4: Claude Code CLI (WSL2)
# ══════════════════════════════════════════════════════════════
Write-Step "4/8 Claude Code CLI"
# 检测 WSL2 原生 claude排除通过 WSL interop 调用的 Windows claude.exe路径含 /mnt/
$claudeVer = Invoke-WSL @'
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
_c=$(which claude 2>/dev/null)
if [ -n "$_c" ] && ! echo "$_c" | grep -q '/mnt/'; then
claude --version 2>/dev/null
else
echo MISSING
fi
true
'@ -IgnoreError
if ($claudeVer -notmatch "MISSING") {
Write-OK "Claude Code 已安装 (WSL2 原生): $($claudeVer.Trim())"
} else {
Write-Info "安装 @anthropic-ai/claude-code (WSL2 原生)..."
$installClaudeCmd = @'
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
. ~/.mcp-proxy.env 2>/dev/null || true
npm install -g @anthropic-ai/claude-code --quiet
which claude
claude --version
true
'@
Invoke-WSL $installClaudeCmd
Write-OK "Claude Code 安装完成"
}
# 写入 Claude 配置WSL2 侧)— 含 allowedTools 完整白名单(自动信任,无需每次确认)
$mcpAllowedTools = @(
# Claude 内置工具
"Bash","Read","Write","Edit","MultiEdit","Glob","Grep","LS","WebFetch","TodoRead","TodoWrite",
# Editor State
"mcp__unity-mcp__unity_editor_ping","mcp__unity-mcp__unity_editor_state","mcp__unity-mcp__unity_project_info",
# Scene
"mcp__unity-mcp__unity_scene_info","mcp__unity-mcp__unity_scene_open","mcp__unity-mcp__unity_scene_save",
"mcp__unity-mcp__unity_scene_new","mcp__unity-mcp__unity_scene_hierarchy","mcp__unity-mcp__unity_scene_stats",
# GameObject
"mcp__unity-mcp__unity_gameobject_create","mcp__unity-mcp__unity_gameobject_delete",
"mcp__unity-mcp__unity_gameobject_info","mcp__unity-mcp__unity_gameobject_set_transform",
"mcp__unity-mcp__unity_gameobject_duplicate","mcp__unity-mcp__unity_gameobject_set_active",
"mcp__unity-mcp__unity_gameobject_reparent",
# Component
"mcp__unity-mcp__unity_component_add","mcp__unity-mcp__unity_component_remove",
"mcp__unity-mcp__unity_component_get_properties","mcp__unity-mcp__unity_component_set_property",
"mcp__unity-mcp__unity_component_set_reference","mcp__unity-mcp__unity_component_batch_wire",
"mcp__unity-mcp__unity_component_get_referenceable",
# Asset
"mcp__unity-mcp__unity_asset_list","mcp__unity-mcp__unity_asset_import",
"mcp__unity-mcp__unity_asset_delete","mcp__unity-mcp__unity_asset_create_prefab",
"mcp__unity-mcp__unity_asset_instantiate_prefab",
# Script & Code
"mcp__unity-mcp__unity_script_create","mcp__unity-mcp__unity_script_read",
"mcp__unity-mcp__unity_script_update","mcp__unity-mcp__unity_execute_code",
# Material
"mcp__unity-mcp__unity_material_create","mcp__unity-mcp__unity_renderer_set_material",
# Build & Play Mode
"mcp__unity-mcp__unity_build","mcp__unity-mcp__unity_play_mode",
# Console & Compilation
"mcp__unity-mcp__unity_console_log","mcp__unity-mcp__unity_console_clear",
"mcp__unity-mcp__unity_get_compilation_errors",
# Editor Actions
"mcp__unity-mcp__unity_execute_menu_item","mcp__unity-mcp__unity_undo",
"mcp__unity-mcp__unity_redo","mcp__unity-mcp__unity_undo_history",
# Selection & Search
"mcp__unity-mcp__unity_selection_get","mcp__unity-mcp__unity_selection_set",
"mcp__unity-mcp__unity_selection_focus_scene_view","mcp__unity-mcp__unity_selection_find_by_type",
"mcp__unity-mcp__unity_search_by_component","mcp__unity-mcp__unity_search_by_tag",
"mcp__unity-mcp__unity_search_by_layer","mcp__unity-mcp__unity_search_by_name",
"mcp__unity-mcp__unity_search_assets","mcp__unity-mcp__unity_search_missing_references",
# Screenshots & Graphics
"mcp__unity-mcp__unity_screenshot_game","mcp__unity-mcp__unity_screenshot_scene",
"mcp__unity-mcp__unity_graphics_scene_capture","mcp__unity-mcp__unity_graphics_game_capture",
# Prefab
"mcp__unity-mcp__unity_prefab_info","mcp__unity-mcp__unity_set_object_reference",
# Packages
"mcp__unity-mcp__unity_packages_list","mcp__unity-mcp__unity_packages_add",
"mcp__unity-mcp__unity_packages_remove","mcp__unity-mcp__unity_packages_search",
"mcp__unity-mcp__unity_packages_info",
# Queue & Multi-Agent
"mcp__unity-mcp__unity_queue_info","mcp__unity-mcp__unity_agents_list","mcp__unity-mcp__unity_agent_log",
# Advanced Tools proxy200+ 工具通过此代理访问)
"mcp__unity-mcp__unity_list_advanced_tools","mcp__unity-mcp__unity_advanced_tool",
# Unity Hub
"mcp__unity-mcp__unity_hub_list_editors","mcp__unity-mcp__unity_hub_available_releases",
"mcp__unity-mcp__unity_hub_install_editor","mcp__unity-mcp__unity_hub_install_modules",
"mcp__unity-mcp__unity_hub_get_install_path","mcp__unity-mcp__unity_hub_set_install_path",
# Multi-Instance & Project Context
"mcp__unity-mcp__unity_list_instances","mcp__unity-mcp__unity_select_instance",
"mcp__unity-mcp__unity_get_project_context"
)
$claudeSettingsJson = if ($UseLmuAuth) {
# 灵眸 / 中转 API使用 env.ANTHROPIC_AUTH_TOKEN避免与 ANTHROPIC_API_KEY 冲突)
[ordered]@{
env = [ordered]@{
ANTHROPIC_BASE_URL = $ANTHROPIC_BASE_URL
ANTHROPIC_AUTH_TOKEN = $ANTHROPIC_AUTH_TOKEN
API_TIMEOUT_MS = "3000000"
CLAUDE_CODE_ATTRIBUTION_HEADER = "0"
}
model = $CLAUDE_MODEL
allowedTools = $mcpAllowedTools
} | ConvertTo-Json -Depth 4
} else {
# Anthropic 官方:使用 model + allowedToolsAPI Key 由环境变量注入
[ordered]@{ model = $CLAUDE_MODEL; allowedTools = $mcpAllowedTools } | ConvertTo-Json -Depth 3
}
$writeClaudeSettingsCmd = @"
mkdir -p ~/.claude
cat > ~/.claude/settings.json << 'SETTINGS'
$claudeSettingsJson
SETTINGS
true
"@
Invoke-WSL $writeClaudeSettingsCmd -IgnoreError | Out-Null
# 写入 bash 环境变量WSL2 侧)
# 灵眸模式auth token 已在 settings.json env 块中,清理旧 bashrc 变量避免冲突
# 官方模式:写入 ANTHROPIC_API_KEY 到 bashrc
$cleanOldVarsCmd = @'
sed -i '/ANTHROPIC_API_KEY/d' ~/.bashrc ~/.profile 2>/dev/null || true
sed -i '/ANTHROPIC_BASE_URL/d' ~/.bashrc ~/.profile 2>/dev/null || true
sed -i '/CLAUDE_MODEL/d' ~/.bashrc ~/.profile 2>/dev/null || true
sed -i '/# Claude Code CLI/d' ~/.bashrc ~/.profile 2>/dev/null || true
true
'@
Invoke-WSL $cleanOldVarsCmd -IgnoreError | Out-Null
if (-not $UseLmuAuth -and $ANTHROPIC_API_KEY) {
$profBlock = "export ANTHROPIC_API_KEY='$ANTHROPIC_API_KEY'\n"
$profBlock += "export ANTHROPIC_BASE_URL='$ANTHROPIC_BASE_URL'\n"
$profBlock += "export CLAUDE_MODEL='$CLAUDE_MODEL'\n"
$addEnvCmd = @"
printf '\n# Claude Code CLI\n$profBlock' >> ~/.bashrc
true
"@
Invoke-WSL $addEnvCmd -IgnoreError | Out-Null
}
# Windows 侧 Claude 配置
$claudeDir = "$env:USERPROFILE\.claude"
if (-not (Test-Path $claudeDir)) { New-Item -ItemType Directory -Path $claudeDir | Out-Null }
$claudeSettingsJson | Set-Content "$claudeDir\settings.json" -Encoding UTF8
if ($UseLmuAuth) {
# 灵眸token 已在 settings.json 中,清除可能冲突的 Windows 用户级变量
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $null, "User")
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", $null, "User")
[System.Environment]::SetEnvironmentVariable("CLAUDE_MODEL", $null, "User")
} elseif ($ANTHROPIC_API_KEY) {
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", $ANTHROPIC_API_KEY, "User")
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", $ANTHROPIC_BASE_URL, "User")
[System.Environment]::SetEnvironmentVariable("CLAUDE_MODEL", $CLAUDE_MODEL, "User")
}
Write-OK "Claude Code 配置已写入Windows + WSL2"
# ══════════════════════════════════════════════════════════════
# Step 5: Unity MCP Server 安装
# ══════════════════════════════════════════════════════════════
Write-Step "5/8 Unity MCP Server"
Write-Info "MCP Server: https://github.com/AnkleBreaker-Studio/unity-mcp-server"
Write-Info "Unity Plugin: https://github.com/AnkleBreaker-Studio/unity-mcp-plugin"
# ── 5a. Windows 侧安装(供 Claude Desktop / Cursor / Windsurf / VS Code────
Write-Info "安装 Windows 侧 MCP Server -> $InstallDir"
if (Test-Path "$InstallDir\.git") {
Write-Info "已存在,执行 git pull..."
Push-Location $InstallDir
try { git pull --quiet 2>&1 | Out-Null } finally { Pop-Location }
} else {
if (Test-Path $InstallDir) { Remove-Item $InstallDir -Recurse -Force }
git clone --quiet --depth 1 https://github.com/AnkleBreaker-Studio/unity-mcp-server.git $InstallDir
}
$winNodeExists = (Get-Command node -ErrorAction SilentlyContinue) -ne $null
if ($winNodeExists) {
Push-Location $InstallDir
try { & npm install --prefer-offline --quiet 2>&1 | Out-Null } finally { Pop-Location }
Write-OK "Windows MCP Server 就绪: $InstallDir"
} else {
Write-Warn "Windows Node.js 未就绪,跳过 Windows 侧 npm install请安装 Node.js 后重新运行)"
}
# ── 5b. WSL2 侧安装(供 Claude Code CLI─────────────────────
Write-Info "安装 WSL2 侧 MCP Server..."
$wslMcpDir = "~/.mcp-servers/unity-mcp-server"
$wslMcpCmd = @'
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
. ~/.mcp-proxy.env 2>/dev/null || true
mkdir -p $HOME/.mcp-servers
if [ -d $HOME/.mcp-servers/unity-mcp-server/.git ]; then
git -C $HOME/.mcp-servers/unity-mcp-server pull --quiet 2>/dev/null || true
else
git clone --quiet --depth 1 https://github.com/AnkleBreaker-Studio/unity-mcp-server.git \
$HOME/.mcp-servers/unity-mcp-server 2>/dev/null \
|| git -c http.proxy="" -c https.proxy="" clone --quiet --depth 1 \
https://github.com/AnkleBreaker-Studio/unity-mcp-server.git \
$HOME/.mcp-servers/unity-mcp-server
fi
if [ -d $HOME/.mcp-servers/unity-mcp-server ]; then
cd $HOME/.mcp-servers/unity-mcp-server
# 修复可能由 root 遗留的权限问题
sudo chown -R $(whoami):$(whoami) . 2>/dev/null || true
npm install --prefer-offline --quiet 2>/dev/null || npm install --quiet
fi
echo MCP_WSL_OK
true
'@
Invoke-WSL $wslMcpCmd
$wslHome = (Invoke-WSL "echo `$HOME" -IgnoreError).Trim()
$wslScript = "$wslHome/.mcp-servers/unity-mcp-server/src/index.js"
Write-OK "WSL2 MCP Server 就绪: $wslScript"
# ── 5c. 写入各 AI 客户端 MCP 配置 ───────────────────────────
$winScript = ($InstallDir -replace "\\","/") + "/src/index.js"
$mcpEntry = @{
command = "node"
args = @($winScript)
env = @{
UNITY_HUB_PATH = $UnityHubPath
UNITY_BRIDGE_PORT = "$BridgePort"
}
}
$wslMcpEntry = @{
command = "wsl"
args = @("-d", $WSL_DISTRO, "--", "node", $wslScript)
env = @{
UNITY_BRIDGE_PORT = "$BridgePort"
}
}
function Merge-McpConfig {
param([string]$ConfigFile, [hashtable]$Entry, [string]$Label)
$dir = Split-Path $ConfigFile
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
# PS5.1 兼容:手动将 PSCustomObject 转为普通 Hashtable
function ConvertTo-Hashtable($obj) {
if ($obj -is [System.Management.Automation.PSCustomObject]) {
$h = @{}
foreach ($p in $obj.PSObject.Properties) { $h[$p.Name] = ConvertTo-Hashtable $p.Value }
return $h
} elseif ($obj -is [System.Collections.IEnumerable] -and $obj -isnot [string]) {
return @($obj | ForEach-Object { ConvertTo-Hashtable $_ })
}
return $obj
}
$cfg = @{ mcpServers = @{} }
if (Test-Path $ConfigFile) {
try {
$raw = Get-Content $ConfigFile -Raw | ConvertFrom-Json
$converted = ConvertTo-Hashtable $raw
if ($converted -is [hashtable]) { $cfg = $converted }
} catch {}
}
if (-not $cfg.ContainsKey("mcpServers")) { $cfg["mcpServers"] = @{} }
$cfg["mcpServers"]["unity-mcp"] = $Entry
$cfg | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile -Encoding UTF8
Write-OK "$Label -> $ConfigFile"
}
# Claude DesktopWindows
Merge-McpConfig "$env:APPDATA\Claude\claude_desktop_config.json" $mcpEntry "Claude Desktop"
# Cursor
Merge-McpConfig "$env:USERPROFILE\.cursor\mcp.json" $mcpEntry "Cursor"
# Windsurf两种路径
$windsurfDir = if (Test-Path "$env:APPDATA\Windsurf") { "$env:APPDATA\Windsurf" } else { "$env:USERPROFILE\.codeium\windsurf" }
Merge-McpConfig "$windsurfDir\mcp_config.json" $mcpEntry "Windsurf"
# VS Code
Merge-McpConfig "$env:APPDATA\Code\User\mcp.json" $mcpEntry "VS Code"
# Claude Code CLIWSL2——用 `claude mcp add --scope user` 写入全局用户级 MCP 配置
$wslMcpCfgCmd = @"
export NVM_DIR="`$HOME/.nvm"
[ -s "`$NVM_DIR/nvm.sh" ] && . "`$NVM_DIR/nvm.sh"
. ~/.mcp-proxy.env 2>/dev/null || true
#
claude mcp remove unity-mcp --scope user 2>/dev/null || true
claude mcp add --scope user unity-mcp node $wslScript -e UNITY_BRIDGE_PORT=$BridgePort
echo "Claude Code MCP configured"
true
"@
Invoke-WSL $wslMcpCfgCmd -IgnoreError | Out-Null
Write-OK "Claude Code CLI (WSL2) MCP -> unity-mcp (node $wslScript)"
# ══════════════════════════════════════════════════════════════
# Step 6: Windows 防火墙放行 MCP Bridge 端口
# ══════════════════════════════════════════════════════════════
Write-Step "6/8 Windows 防火墙规则"
if ($SkipFirewall) {
Write-Warn "已跳过防火墙配置(-SkipFirewall"
} else {
$portRange = "$BridgePort-$($BridgePort + 9)"
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if ($isAdmin) {
foreach ($dir in @("Inbound","Outbound")) {
$name = "Unity MCP Bridge $dir"
Get-NetFirewallRule -DisplayName $name -ErrorAction SilentlyContinue |
Remove-NetFirewallRule -ErrorAction SilentlyContinue
New-NetFirewallRule -DisplayName $name -Direction $dir `
-LocalPort $portRange -Protocol TCP -Action Allow -Profile Any | Out-Null
Write-OK "防火墙规则: $name (TCP $portRange)"
}
} else {
Write-Warn "非管理员权限,使用提升权限添加防火墙规则..."
$cmd = @"
`$p = '$portRange'
foreach (`$d in @('Inbound','Outbound')) {
`$n = "Unity MCP Bridge `$d"
Get-NetFirewallRule -DisplayName `$n -ErrorAction SilentlyContinue | Remove-NetFirewallRule -ErrorAction SilentlyContinue
New-NetFirewallRule -DisplayName `$n -Direction `$d -LocalPort `$p -Protocol TCP -Action Allow -Profile Any | Out-Null
Write-Host "OK: `$n"
}
Start-Sleep 1
"@
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cmd))
$proc = Start-Process powershell.exe -ArgumentList "-NoProfile -EncodedCommand $enc" `
-Verb RunAs -Wait -PassThru
if ($proc.ExitCode -eq 0) {
Write-OK "防火墙规则已添加 (TCP $portRange)"
} else {
Write-Warn "防火墙配置可能未成功,请手动放行 TCP $portRange"
}
}
}
# ══════════════════════════════════════════════════════════════
# Step 7: Rust 工具链 & Token Killer (WSL2)
# ══════════════════════════════════════════════════════════════
Write-Step "7/8 Rust & Token Killer"
$rustVer = Invoke-WSL ". ~/.cargo/env 2>/dev/null; rustc --version 2>/dev/null || echo MISSING" -IgnoreError
if ($rustVer -notmatch "MISSING") {
Write-OK "Rust 已安装: $($rustVer.Trim())"
} else {
Write-Info "安装 Rust 工具链..."
$installRustCmd = @"
sudo apt-get install -y -qq build-essential pkg-config libssl-dev 2>/dev/null || true
# mirrored 127.0.0.1rustup env
. ~/.mcp-proxy.env 2>/dev/null || true
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup-init.sh
sh /tmp/rustup-init.sh -y --default-toolchain stable --no-modify-path
source ~/.cargo/env
rustc --version
"@
Invoke-WSL $installRustCmd
Invoke-WSL "grep -q 'cargo/env' ~/.bashrc || echo 'source ~/.cargo/env 2>/dev/null || true' >> ~/.bashrc" -IgnoreError | Out-Null
Invoke-WSL "grep -q 'cargo/env' ~/.profile || echo 'source ~/.cargo/env 2>/dev/null || true' >> ~/.profile" -IgnoreError | Out-Null
Write-OK "Rust 安装完成"
}
$rtkVer = Invoke-WSL ". ~/.cargo/env 2>/dev/null; rtk --version 2>/dev/null || echo MISSING" -IgnoreError
if ($rtkVer -notmatch "MISSING") {
Write-OK "rtk 已安装: $($rtkVer.Trim())"
} else {
Write-Info "安装 rtk (Rust Token Killer)..."
$installRtkCmd = @"
. ~/.cargo/env 2>/dev/null || true
# mirrored CARGO_NET_GIT_FETCH_WITH_CLI cargo git
# git clone github git proxy https
_saved_http=`$(git config --global http.proxy 2>/dev/null)
_saved_https=`$(git config --global https.proxy 2>/dev/null)
git config --global --unset http.proxy 2>/dev/null || true
git config --global --unset https.proxy 2>/dev/null || true
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install --git https://github.com/rtk-ai/rtk 2>&1 | tail -5
[ -n "`$_saved_http" ] && git config --global http.proxy "`$_saved_http" || true
[ -n "`$_saved_https" ] && git config --global https.proxy "`$_saved_https" || true
"@
Invoke-WSL $installRtkCmd -IgnoreError
$rtkCheck = Invoke-WSL ". ~/.cargo/env 2>/dev/null; rtk --version 2>/dev/null || echo FAILED" -IgnoreError
if ($rtkCheck -match "FAILED") {
Write-Warn "rtk 安装失败,可手动运行: cargo install --git https://github.com/rtk-ai/rtk"
} else {
Write-OK "rtk 安装成功: $($rtkCheck.Trim())"
}
}
# rtk init -g安装 Claude Code PreToolUse hook幂等
Invoke-WSL ". ~/.cargo/env 2>/dev/null; rtk init -g --auto-patch 2>/dev/null || true" -IgnoreError | Out-Null
Write-OK "rtk hook 已注册 (rtk init -g),重启 Claude Code 后生效"
# ══════════════════════════════════════════════════════════════
# Step 8: PowerShell Profile 配置
# ══════════════════════════════════════════════════════════════
Write-Step "8/8 PowerShell Profile"
$pwshProfile = "$env:USERPROFILE\Documents\PowerShell\Microsoft.PowerShell_profile.ps1"
$profileDir = Split-Path $pwshProfile
if (-not (Test-Path $profileDir)) { New-Item -ItemType Directory -Path $profileDir | Out-Null }
$profileBlock = @"
# Claude Dev Stack (generated by deploy.ps1)
`$env:ANTHROPIC_BASE_URL = "$ANTHROPIC_BASE_URL"
$(if ($ANTHROPIC_API_KEY) { "`$env:ANTHROPIC_API_KEY = `"$ANTHROPIC_API_KEY`"" } else { "# ANTHROPIC_API_KEY= (配置 .env 后重新运行)" })
`$env:CLAUDE_MODEL = "$CLAUDE_MODEL"
function claude-wsl { wsl -d $WSL_DISTRO -- bash -ic 'claude' }
function unity-mcp-status { Invoke-RestMethod http://127.0.0.1:$BridgePort/api/ping -ErrorAction SilentlyContinue }
#
"@
$existing = if (Test-Path $pwshProfile) { Get-Content $pwshProfile -Raw } else { "" }
if ($existing -match "Claude Dev Stack") {
$existing = $existing -replace "(?ms)# ── Claude Dev Stack.*?# ─{60}", $profileBlock
Set-Content $pwshProfile $existing -Encoding UTF8
} else {
Add-Content $pwshProfile $profileBlock -Encoding UTF8
}
$policy = Get-ExecutionPolicy -Scope CurrentUser
if ($policy -in @("Restricted","Undefined")) {
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force
}
Write-OK "PowerShell Profile 已配置: $pwshProfile"
# ══════════════════════════════════════════════════════════════
# 安装总结
# ══════════════════════════════════════════════════════════════
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════════╗" -ForegroundColor Green
Write-Host "║ ✅ 全栈部署完成 ║" -ForegroundColor Green
Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Green
$items = @(
@("WSL2 内核", (Invoke-WSL "uname -r 2>/dev/null" -IgnoreError).Trim()),
@("Node.js (Win)", (& node --version 2>$null)),
@("Node.js (WSL)", $(if ($nodeVer -match "v\d+") { $nodeVer.Trim() } else { "" })),
@("Claude Code", $(if ($claudeVer -notmatch "MISSING") { $claudeVer.Trim() } else { "" })),
@("Rust", (Invoke-WSL ". ~/.cargo/env && rustc --version 2>/dev/null" -IgnoreError).Trim()),
@("rtk", (Invoke-WSL ". ~/.cargo/env && rtk --version 2>/dev/null" -IgnoreError).Trim()),
@("MCP Server", $InstallDir),
@("Bridge Port", "TCP $BridgePort (防火墙已放行 $BridgePort-$($BridgePort+9))")
)
foreach ($it in $items) {
Write-Host (" {0,-16} {1}" -f $it[0], $(if ($it[1]) { $it[1] } else { "(未安装)" })) -ForegroundColor White
}
if ($resolvedProxyPort -gt 0) {
Write-Host ""
Write-Host " v2rayN 代理 127.0.0.1:$resolvedProxyPort (WSL2 通过 Windows 主机 IP 访问)" -ForegroundColor DarkGray
}
Write-Host ""
Write-Host " ━━━━ 后续步骤 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
Write-Host " 1) 在 Unity 项目中安装 MCP Plugin每个项目一次" -ForegroundColor White
Write-Host " Window > Package Manager > + > Add from git URL:" -ForegroundColor DarkGray
Write-Host " https://github.com/AnkleBreaker-Studio/unity-mcp-plugin.git" -ForegroundColor Yellow
Write-Host ""
Write-Host " 2) 打开 Unity 后确认 Bridge 在线(浏览器验证):" -ForegroundColor White
Write-Host " http://127.0.0.1:$BridgePort/api/ping" -ForegroundColor Yellow
Write-Host ""
Write-Host " 3) 重启 AI 客户端Claude Desktop / Cursor / Windsurf" -ForegroundColor White
Write-Host " MCP 配置已自动写入各客户端配置文件" -ForegroundColor DarkGray
Write-Host ""
Write-Host " 4) 在 Claude Code (WSL2) 中使用:" -ForegroundColor White
Write-Host " claude-wsl → claude → /mcp" -ForegroundColor DarkGray
Write-Host ""
if (-not $ANTHROPIC_API_KEY) {
Write-Warn " ⚠ 未设置 ANTHROPIC_API_KEY请编辑 .env 后重新运行"
}
Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Green