Files
server-deploy/windows-dev-stack/deploy.ps1
2026-05-29 09:22:34 +08:00

798 lines
45 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
Windows 原生 Claude Code CLI + GitHub Copilot CLI + MCP + Rust Token Killer 一键部署脚本
.DESCRIPTION
不依赖 WSL2全部组件原生运行在 Windows 上。完成以下步骤:
1. Node.js LTS 检测 / 安装winget
2. Claude Code CLI (@anthropic-ai/claude-code) 安装
3. GitHub CLI + Copilot CLI 扩展安装winget + gh extension
4. Unity MCP Server 安装 & 写入各 AI 客户端 MCP 配置
5. Godot MCP Pro 安装(本地包)& 写入各 AI 客户端 MCP 配置
6. Rust 工具链 + RTK (Rust Token Killer) 安装
7. RTK hook 注册到 Claude Code + Copilot Instructions 生成
8. PowerShell Profile 快捷命令写入
.PARAMETER BridgePort
Unity MCP Bridge 端口,默认 7890
.PARAMETER SkipFirewall
跳过防火墙规则配置
.NOTES
防火墙规则需要管理员权限,其余步骤普通用户即可运行。
脚本可重复运行(幂等),已安装的组件自动跳过。
#>
param(
[int] $BridgePort = 7890,
[string]$GodotMcpVersion = "godot-mcp-pro-v1.14.1",
[switch]$SkipFirewall
)
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
}
}
$Cfg = @{}
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("'")
$Cfg[$key] = $val
}
Write-Info "已加载配置:$EnvFile"
}
$ANTHROPIC_API_KEY = $Cfg["ANTHROPIC_API_KEY"] ?? ""
$ANTHROPIC_AUTH_TOKEN = $Cfg["ANTHROPIC_AUTH_TOKEN"] ?? ""
$ANTHROPIC_BASE_URL = $Cfg["ANTHROPIC_BASE_URL"] ?? "https://api.lmuai.com"
$CLAUDE_MODEL = $Cfg["CLAUDE_MODEL"] ?? "claude-sonnet-4-6"
$BRIDGE_PORT_CFG = $Cfg["BRIDGE_PORT"] ?? "$BridgePort"
$UnityHubPath = $Cfg["UNITY_HUB_PATH"] ?? "C:\Program Files\Unity Hub\Unity Hub.exe"
$UnityMcpInstallDir = $Cfg["UNITY_MCP_INSTALL_DIR"] ?? "$env:USERPROFILE\unity-mcp-server"
$GodotMcpPackageDir = $Cfg["GODOT_MCP_PACKAGE_DIR"] ?? "."
# 参数 BridgePort 优先于 .env
if ($BridgePort -eq 7890 -and $BRIDGE_PORT_CFG -ne "7890") { $BridgePort = [int]$BRIDGE_PORT_CFG }
# 判断认证方式
$UseLmuAuth = ($ANTHROPIC_AUTH_TOKEN -ne "" -and $ANTHROPIC_BASE_URL -ne "https://api.anthropic.com")
# ──────────────────────────────────────────────────────────────
# Banner
# ──────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║ Windows Native: Claude Code + Copilot CLI + MCP + RTK ║" -ForegroundColor Cyan
Write-Host "║ Unity MCP · Godot MCP Pro · Rust Token Killer ║" -ForegroundColor Cyan
Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
# ──────────────────────────────────────────────────────────────
# 辅助:刷新当前进程 PATH
# ──────────────────────────────────────────────────────────────
function Refresh-Path {
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("Path", "User")
}
# ──────────────────────────────────────────────────────────────
# 辅助winget 安装(静默)
# ──────────────────────────────────────────────────────────────
function Install-Winget {
param([string]$Id, [string]$DisplayName)
# pwsh (Core) 里 winget 不在 PATH尝试固定路径
$wgPath = (Get-Command winget -ErrorAction SilentlyContinue)?.Source
if (-not $wgPath) {
$wgPath = "$env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe"
if (-not (Test-Path $wgPath)) { $wgPath = $null }
}
if (-not $wgPath) {
Write-Warn "未找到 winget请手动安装 $DisplayName"
return $false
}
Write-Info "winget install $Id ..."
& $wgPath install --id $Id --silent --accept-source-agreements --accept-package-agreements 2>&1 | Out-Null
Refresh-Path
return $true
}
# ──────────────────────────────────────────────────────────────
# 辅助:写入 / 合并 MCP 配置文件JSON
# ──────────────────────────────────────────────────────────────
function Merge-McpConfig {
param(
[string] $ConfigFile,
[hashtable] $Entry,
[string] $Label,
[string] $ServerName = "unity-mcp"
)
$dir = Split-Path $ConfigFile
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
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"][$ServerName] = $Entry
$cfg | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile -Encoding UTF8
Write-OK "$Label -> $ConfigFile"
}
# ──────────────────────────────────────────────────────────────
# 辅助:写入 VS Code settings.json 中的 mcp.serversVS Code 专用格式)
# VS Code Copilot Agent 读取 settings.json -> "mcp" -> "servers"
# 而非 mcp.json 中的 "mcpServers"(那是 Claude Desktop / Cursor 格式)
# ──────────────────────────────────────────────────────────────
function Merge-VsCodeMcpSettings {
param(
[string] $ServerName,
[hashtable] $Entry,
[string] $Label
)
$settingsFile = "$env:APPDATA\Code\User\settings.json"
$dir = Split-Path $settingsFile
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
function ConvertTo-OrderedHashtable($obj) {
if ($obj -is [System.Management.Automation.PSCustomObject]) {
$h = [ordered]@{}
foreach ($p in $obj.PSObject.Properties) { $h[$p.Name] = ConvertTo-OrderedHashtable $p.Value }
return $h
} elseif ($obj -is [System.Collections.IEnumerable] -and $obj -isnot [string]) {
return @($obj | ForEach-Object { ConvertTo-OrderedHashtable $_ })
}
return $obj
}
$cfg = [ordered]@{}
if (Test-Path $settingsFile) {
try {
$raw = Get-Content $settingsFile -Raw | ConvertFrom-Json
$cfg = ConvertTo-OrderedHashtable $raw
} catch {
Write-Warn "settings.json 解析失败,将新建 mcp 节点:$_"
}
}
if (-not $cfg.ContainsKey("mcp")) { $cfg["mcp"] = [ordered]@{} }
if (-not $cfg["mcp"].ContainsKey("servers")) { $cfg["mcp"]["servers"] = [ordered]@{} }
$cfg["mcp"]["servers"][$ServerName] = $Entry
$cfg | ConvertTo-Json -Depth 15 | Set-Content $settingsFile -Encoding UTF8
Write-OK "$Label -> $settingsFile [mcp.servers.$ServerName]"
}
# ══════════════════════════════════════════════════════════════
# Step 1: Node.js LTS
# ══════════════════════════════════════════════════════════════
Write-Step "1/8 Node.js LTS"
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue
if ($nodeCmd) {
Write-OK "Node.js 已安装: $(& node --version)"
} else {
Write-Info "通过 winget 安装 Node.js LTS..."
$ok = Install-Winget "OpenJS.NodeJS.LTS" "Node.js LTS"
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue
if ($nodeCmd) {
Write-OK "Node.js 安装完成: $(& node --version)"
} else {
Write-Warn "未能自动安装 Node.js请手动安装后重新运行"
Write-Warn " https://nodejs.org/zh-cn/download 或 winget install OpenJS.NodeJS.LTS"
}
}
# ══════════════════════════════════════════════════════════════
# Step 2: Claude Code CLI
# ══════════════════════════════════════════════════════════════
Write-Step "2/8 Claude Code CLI"
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
if ($claudeCmd) {
Write-OK "Claude Code 已安装: $(& claude --version 2>&1 | Select-Object -First 1)"
} else {
if (Get-Command node -ErrorAction SilentlyContinue) {
Write-Info "安装 @anthropic-ai/claude-code ..."
& npm install -g @anthropic-ai/claude-code --quiet 2>&1 | Out-Null
Refresh-Path
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
if ($claudeCmd) {
Write-OK "Claude Code 安装完成: $(& claude --version 2>&1 | Select-Object -First 1)"
} else {
Write-Warn "安装后未找到 claude 命令,请重开终端后重试"
}
} else {
Write-Warn "Node.js 未就绪,跳过 Claude Code 安装"
}
}
# ── 写入 Claude Code 配置settings.json──────────────────
$mcpAllowedTools = @(
"Bash","Read","Write","Edit","MultiEdit","Glob","Grep","LS","WebFetch","TodoRead","TodoWrite",
# Unity MCP
"mcp__unity-mcp__unity_editor_ping","mcp__unity-mcp__unity_editor_state","mcp__unity-mcp__unity_project_info",
"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",
"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",
"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",
"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",
"mcp__unity-mcp__unity_script_create","mcp__unity-mcp__unity_script_read",
"mcp__unity-mcp__unity_script_update","mcp__unity-mcp__unity_execute_code",
"mcp__unity-mcp__unity_material_create","mcp__unity-mcp__unity_renderer_set_material",
"mcp__unity-mcp__unity_build","mcp__unity-mcp__unity_play_mode",
"mcp__unity-mcp__unity_console_log","mcp__unity-mcp__unity_console_clear",
"mcp__unity-mcp__unity_get_compilation_errors",
"mcp__unity-mcp__unity_execute_menu_item","mcp__unity-mcp__unity_undo",
"mcp__unity-mcp__unity_redo","mcp__unity-mcp__unity_undo_history",
"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",
"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",
"mcp__unity-mcp__unity_prefab_info","mcp__unity-mcp__unity_set_object_reference",
"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",
"mcp__unity-mcp__unity_queue_info","mcp__unity-mcp__unity_agents_list","mcp__unity-mcp__unity_agent_log",
"mcp__unity-mcp__unity_list_advanced_tools","mcp__unity-mcp__unity_advanced_tool",
"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",
"mcp__unity-mcp__unity_list_instances","mcp__unity-mcp__unity_select_instance",
"mcp__unity-mcp__unity_get_project_context",
# Godot MCP Pro
"mcp__godot-mcp-pro__get_project_info","mcp__godot-mcp-pro__get_filesystem_tree",
"mcp__godot-mcp-pro__search_files","mcp__godot-mcp-pro__search_in_files",
"mcp__godot-mcp-pro__get_project_settings","mcp__godot-mcp-pro__get_project_statistics",
"mcp__godot-mcp-pro__uid_to_project_path","mcp__godot-mcp-pro__project_path_to_uid",
"mcp__godot-mcp-pro__get_scene_tree","mcp__godot-mcp-pro__get_scene_file_content",
"mcp__godot-mcp-pro__get_scene_exports","mcp__godot-mcp-pro__get_scene_dependencies",
"mcp__godot-mcp-pro__create_scene","mcp__godot-mcp-pro__open_scene",
"mcp__godot-mcp-pro__save_scene","mcp__godot-mcp-pro__add_scene_instance",
"mcp__godot-mcp-pro__get_node_properties","mcp__godot-mcp-pro__get_node_groups",
"mcp__godot-mcp-pro__get_signals","mcp__godot-mcp-pro__find_nodes_in_group",
"mcp__godot-mcp-pro__find_nodes_by_type","mcp__godot-mcp-pro__find_signal_connections",
"mcp__godot-mcp-pro__find_node_references",
"mcp__godot-mcp-pro__add_node","mcp__godot-mcp-pro__duplicate_node",
"mcp__godot-mcp-pro__move_node","mcp__godot-mcp-pro__rename_node",
"mcp__godot-mcp-pro__update_property","mcp__godot-mcp-pro__add_resource",
"mcp__godot-mcp-pro__set_anchor_preset",
"mcp__godot-mcp-pro__connect_signal","mcp__godot-mcp-pro__disconnect_signal",
"mcp__godot-mcp-pro__set_node_groups",
"mcp__godot-mcp-pro__batch_set_property","mcp__godot-mcp-pro__cross_scene_set_property",
"mcp__godot-mcp-pro__list_scripts","mcp__godot-mcp-pro__read_script",
"mcp__godot-mcp-pro__get_open_scripts","mcp__godot-mcp-pro__validate_script",
"mcp__godot-mcp-pro__create_script","mcp__godot-mcp-pro__edit_script",
"mcp__godot-mcp-pro__attach_script",
"mcp__godot-mcp-pro__get_editor_errors","mcp__godot-mcp-pro__get_output_log",
"mcp__godot-mcp-pro__get_editor_screenshot","mcp__godot-mcp-pro__get_editor_performance",
"mcp__godot-mcp-pro__clear_output","mcp__godot-mcp-pro__reload_plugin",
"mcp__godot-mcp-pro__reload_project",
"mcp__godot-mcp-pro__read_resource","mcp__godot-mcp-pro__get_resource_preview",
"mcp__godot-mcp-pro__create_resource","mcp__godot-mcp-pro__edit_resource",
"mcp__godot-mcp-pro__read_shader","mcp__godot-mcp-pro__get_shader_params",
"mcp__godot-mcp-pro__create_shader","mcp__godot-mcp-pro__edit_shader",
"mcp__godot-mcp-pro__assign_shader_material","mcp__godot-mcp-pro__set_shader_param",
"mcp__godot-mcp-pro__list_animations","mcp__godot-mcp-pro__get_animation_info",
"mcp__godot-mcp-pro__create_animation","mcp__godot-mcp-pro__add_animation_track",
"mcp__godot-mcp-pro__set_animation_keyframe",
"mcp__godot-mcp-pro__get_animation_tree_structure",
"mcp__godot-mcp-pro__create_animation_tree","mcp__godot-mcp-pro__add_state_machine_state",
"mcp__godot-mcp-pro__add_state_machine_transition",
"mcp__godot-mcp-pro__set_blend_tree_node","mcp__godot-mcp-pro__set_tree_parameter",
"mcp__godot-mcp-pro__get_audio_bus_layout","mcp__godot-mcp-pro__get_audio_info",
"mcp__godot-mcp-pro__add_audio_bus","mcp__godot-mcp-pro__set_audio_bus",
"mcp__godot-mcp-pro__add_audio_bus_effect","mcp__godot-mcp-pro__add_audio_player",
"mcp__godot-mcp-pro__get_input_actions","mcp__godot-mcp-pro__set_input_action",
"mcp__godot-mcp-pro__get_physics_layers","mcp__godot-mcp-pro__get_collision_info",
"mcp__godot-mcp-pro__set_physics_layers",
"mcp__godot-mcp-pro__add_mesh_instance","mcp__godot-mcp-pro__setup_lighting",
"mcp__godot-mcp-pro__set_material_3d","mcp__godot-mcp-pro__setup_environment",
"mcp__godot-mcp-pro__setup_camera_3d","mcp__godot-mcp-pro__add_gridmap",
"mcp__godot-mcp-pro__setup_collision","mcp__godot-mcp-pro__add_raycast",
"mcp__godot-mcp-pro__setup_physics_body",
"mcp__godot-mcp-pro__get_navigation_info",
"mcp__godot-mcp-pro__setup_navigation_region","mcp__godot-mcp-pro__bake_navigation_mesh",
"mcp__godot-mcp-pro__setup_navigation_agent","mcp__godot-mcp-pro__set_navigation_layers",
"mcp__godot-mcp-pro__get_particle_info",
"mcp__godot-mcp-pro__create_particles","mcp__godot-mcp-pro__set_particle_material",
"mcp__godot-mcp-pro__set_particle_color_gradient","mcp__godot-mcp-pro__apply_particle_preset",
"mcp__godot-mcp-pro__get_theme_info",
"mcp__godot-mcp-pro__create_theme","mcp__godot-mcp-pro__set_theme_color",
"mcp__godot-mcp-pro__set_theme_constant","mcp__godot-mcp-pro__set_theme_font_size",
"mcp__godot-mcp-pro__set_theme_stylebox",
"mcp__godot-mcp-pro__tilemap_get_cell","mcp__godot-mcp-pro__tilemap_get_info",
"mcp__godot-mcp-pro__tilemap_get_used_cells",
"mcp__godot-mcp-pro__tilemap_set_cell","mcp__godot-mcp-pro__tilemap_fill_rect",
"mcp__godot-mcp-pro__get_autoload","mcp__godot-mcp-pro__add_autoload",
"mcp__godot-mcp-pro__set_project_setting",
"mcp__godot-mcp-pro__find_unused_resources","mcp__godot-mcp-pro__analyze_signal_flow",
"mcp__godot-mcp-pro__analyze_scene_complexity","mcp__godot-mcp-pro__find_script_references",
"mcp__godot-mcp-pro__detect_circular_dependencies",
"mcp__godot-mcp-pro__get_performance_monitors",
"mcp__godot-mcp-pro__get_export_info","mcp__godot-mcp-pro__list_export_presets",
"mcp__godot-mcp-pro__play_scene","mcp__godot-mcp-pro__stop_scene",
"mcp__godot-mcp-pro__get_game_scene_tree","mcp__godot-mcp-pro__get_game_node_properties",
"mcp__godot-mcp-pro__get_game_screenshot","mcp__godot-mcp-pro__set_game_node_property",
"mcp__godot-mcp-pro__find_nodes_by_script",
"mcp__godot-mcp-pro__simulate_key","mcp__godot-mcp-pro__simulate_mouse_click",
"mcp__godot-mcp-pro__simulate_mouse_move","mcp__godot-mcp-pro__simulate_action",
"mcp__godot-mcp-pro__simulate_sequence",
"mcp__godot-mcp-pro__capture_frames","mcp__godot-mcp-pro__record_frames",
"mcp__godot-mcp-pro__monitor_properties","mcp__godot-mcp-pro__batch_get_properties",
"mcp__godot-mcp-pro__start_recording","mcp__godot-mcp-pro__stop_recording",
"mcp__godot-mcp-pro__replay_recording",
"mcp__godot-mcp-pro__find_ui_elements","mcp__godot-mcp-pro__click_button_by_text",
"mcp__godot-mcp-pro__wait_for_node","mcp__godot-mcp-pro__find_nearby_nodes",
"mcp__godot-mcp-pro__navigate_to","mcp__godot-mcp-pro__move_to",
"mcp__godot-mcp-pro__run_test_scenario","mcp__godot-mcp-pro__assert_node_state",
"mcp__godot-mcp-pro__assert_screen_text","mcp__godot-mcp-pro__run_stress_test",
"mcp__godot-mcp-pro__get_test_report","mcp__godot-mcp-pro__compare_screenshots"
)
$claudeSettingsJson = if ($UseLmuAuth) {
[ordered]@{
env = [ordered]@{
ANTHROPIC_BASE_URL = $ANTHROPIC_BASE_URL
ANTHROPIC_AUTH_TOKEN = $ANTHROPIC_AUTH_TOKEN
API_TIMEOUT_MS = "3000000"
CLAUDE_CODE_ATTRIBUTION_HEADER = "0"
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1"
}
model = $CLAUDE_MODEL
allowedTools = $mcpAllowedTools
} | ConvertTo-Json -Depth 4
} else {
[ordered]@{ model = $CLAUDE_MODEL; allowedTools = $mcpAllowedTools } | ConvertTo-Json -Depth 3
}
$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) {
[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 配置已写入 $claudeDir\settings.json"
# ══════════════════════════════════════════════════════════════
# Step 3: GitHub CLI + Copilot CLI 扩展
# ══════════════════════════════════════════════════════════════
Write-Step "3/8 GitHub CLI + Copilot CLI"
$ghCmd = Get-Command gh -ErrorAction SilentlyContinue
if ($ghCmd) {
Write-OK "GitHub CLI 已安装: $(& gh --version 2>&1 | Select-Object -First 1)"
} else {
Write-Info "通过 winget 安装 GitHub CLI..."
$ok = Install-Winget "GitHub.cli" "GitHub CLI"
$ghCmd = Get-Command gh -ErrorAction SilentlyContinue
if ($ghCmd) {
Write-OK "GitHub CLI 安装完成: $(& gh --version 2>&1 | Select-Object -First 1)"
} else {
Write-Warn "GitHub CLI 安装后未找到请重开终端或手动安装winget install GitHub.cli"
}
}
if ($ghCmd) {
$extList = & gh extension list 2>&1 | Out-String
if ($extList -match "github/gh-copilot") {
Write-OK "GitHub Copilot CLI 扩展已安装"
} else {
Write-Info "安装 GitHub Copilot CLI 扩展..."
try {
& gh extension install github/gh-copilot 2>&1 | Out-Null
Write-OK "GitHub Copilot CLI 扩展安装完成入口gh copilot"
} catch {
Write-Warn "安装失败(可能需要先 gh auth login请手动运行gh extension install github/gh-copilot"
}
}
}
# ══════════════════════════════════════════════════════════════
# Step 4: Unity MCP Server
# ══════════════════════════════════════════════════════════════
Write-Step "4/8 Unity MCP Server"
Write-Info "安装目录: $UnityMcpInstallDir"
$gitCmd = Get-Command git -ErrorAction SilentlyContinue
if (-not $gitCmd) {
Write-Warn "未找到 git跳过 Unity MCP Server 安装"
Write-Warn "请安装 Git for Windowswinget install Git.Git"
$UnityMcpReady = $false
} else {
if (Test-Path "$UnityMcpInstallDir\.git") {
Write-Info "已存在,更新至最新..."
Push-Location $UnityMcpInstallDir
try { & git pull --quiet 2>&1 | Out-Null } finally { Pop-Location }
} else {
if (Test-Path $UnityMcpInstallDir) { Remove-Item $UnityMcpInstallDir -Recurse -Force }
& git clone --quiet --depth 1 https://github.com/AnkleBreaker-Studio/unity-mcp-server.git $UnityMcpInstallDir
}
if (Get-Command node -ErrorAction SilentlyContinue) {
Push-Location $UnityMcpInstallDir
try { & npm install --prefer-offline --quiet 2>&1 | Out-Null } finally { Pop-Location }
Write-OK "Unity MCP Server 就绪"
$UnityMcpReady = $true
} else {
Write-Warn "Node.js 未就绪,跳过 npm install"
$UnityMcpReady = $false
}
}
# ── 确定入口文件 ──────────────────────────────────────────────
$unityEntry = ""
if ($UnityMcpReady) {
foreach ($f in @("src/index.js","build/index.js","index.js")) {
$fp = Join-Path $UnityMcpInstallDir $f
if (Test-Path $fp) { $unityEntry = $fp -replace "\\","/"; break }
}
}
# ── 写入 MCP 配置(四大 AI 客户端 + Claude Code CLI─────────
if ($unityEntry) {
$unityMcpEntry = @{
command = "node"
args = @($unityEntry)
env = @{ UNITY_HUB_PATH = $UnityHubPath; UNITY_BRIDGE_PORT = "$BridgePort" }
}
Merge-McpConfig "$env:APPDATA\Claude\claude_desktop_config.json" $unityMcpEntry "Claude Desktop"
Merge-McpConfig "$env:USERPROFILE\.cursor\mcp.json" $unityMcpEntry "Cursor"
$wsDirC = if (Test-Path "$env:APPDATA\Windsurf") { "$env:APPDATA\Windsurf" } else { "$env:USERPROFILE\.codeium\windsurf" }
Merge-McpConfig "$wsDirC\mcp_config.json" $unityMcpEntry "Windsurf"
Merge-VsCodeMcpSettings "unity-mcp" $unityMcpEntry "VS Code Copilot Agent (Unity MCP)"
# Claude Code CLIWindows
if (Get-Command claude -ErrorAction SilentlyContinue) {
& claude mcp remove unity-mcp --scope user 2>&1 | Out-Null
& claude mcp add --scope user unity-mcp node $unityEntry -e "UNITY_BRIDGE_PORT=$BridgePort" 2>&1 | Out-Null
Write-OK "Claude Code CLI MCP -> unity-mcp"
}
}
# ══════════════════════════════════════════════════════════════
# Step 5: Godot MCP Pro
# ══════════════════════════════════════════════════════════════
Write-Step "5/8 Godot MCP Pro"
# 解析 Godot MCP Pro 本地包路径(相对于本脚本目录)
$godotPkgBase = if ([System.IO.Path]::IsPathRooted($GodotMcpPackageDir)) {
$GodotMcpPackageDir
} else {
[System.IO.Path]::GetFullPath((Join-Path $ScriptDir $GodotMcpPackageDir))
}
$godotSrcDir = Join-Path $godotPkgBase "$GodotMcpVersion\server"
$godotWinDir = "$env:USERPROFILE\godot-mcp-pro"
if (-not (Test-Path $godotSrcDir)) {
Write-Warn "未找到 Godot MCP Pro 源目录: $godotSrcDir"
Write-Warn "请确认 .env 中 GODOT_MCP_PACKAGE_DIR 指向包含 $GodotMcpVersion\ 的目录"
$godotEntry = ""
} else {
Write-Info "同步 server/ -> $godotWinDir"
$rcArgs = @($godotSrcDir, $godotWinDir, "/E", "/XD", "node_modules",
"/NFL", "/NDL", "/NJH", "/NJS", "/nc", "/ns", "/np")
& robocopy @rcArgs 2>&1 | Out-Null
if (Get-Command node -ErrorAction SilentlyContinue) {
Push-Location $godotWinDir
try { & node build/setup.js install 2>&1 | Out-Null } finally { Pop-Location }
Write-OK "Godot MCP Pro 就绪: $godotWinDir"
} else {
Write-Warn "Node.js 未就绪,跳过 Godot MCP Pro setup"
}
$godotEntry = ($godotWinDir -replace "\\","/") + "/build/index.js"
$godotMcpEntry = @{ command = "node"; args = @($godotEntry) }
Merge-McpConfig "$env:APPDATA\Claude\claude_desktop_config.json" $godotMcpEntry "Claude Desktop (Godot)" "godot-mcp-pro"
Merge-McpConfig "$env:USERPROFILE\.cursor\mcp.json" $godotMcpEntry "Cursor (Godot)" "godot-mcp-pro"
$wsDirG = if (Test-Path "$env:APPDATA\Windsurf") { "$env:APPDATA\Windsurf" } else { "$env:USERPROFILE\.codeium\windsurf" }
Merge-McpConfig "$wsDirG\mcp_config.json" $godotMcpEntry "Windsurf (Godot)" "godot-mcp-pro"
Merge-VsCodeMcpSettings "godot-mcp-pro" $godotMcpEntry "VS Code Copilot Agent (Godot MCP Pro)"
if (Get-Command claude -ErrorAction SilentlyContinue) {
& claude mcp remove godot-mcp-pro --scope user 2>&1 | Out-Null
& claude mcp add --scope user godot-mcp-pro node $godotEntry 2>&1 | Out-Null
Write-OK "Claude Code CLI MCP -> godot-mcp-pro"
}
}
# ══════════════════════════════════════════════════════════════
# Step 6: 防火墙放行 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 "非管理员权限,尝试以提升权限添加防火墙规则..."
$fwCmd = @"
`$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($fwCmd))
$proc = Start-Process powershell.exe -ArgumentList "-NoProfile -EncodedCommand $enc" `
-Verb RunAs -Wait -PassThru -ErrorAction SilentlyContinue
if ($proc -and $proc.ExitCode -eq 0) {
Write-OK "防火墙规则已添加 (TCP $portRange)"
} else {
Write-Warn "防火墙配置未成功请手动以管理员运行New-NetFirewallRule ... -LocalPort $portRange"
}
}
}
# ══════════════════════════════════════════════════════════════
# Step 7: Rust 工具链 & RTK
# ══════════════════════════════════════════════════════════════
Write-Step "7/8 Rust 工具链 & Token Killer"
# 若 .env 配置了国内镜像,提前注入环境变量
$rustupMirror = $Cfg["RUSTUP_DIST_SERVER"] ?? ""
$rustupRoot = $Cfg["RUSTUP_UPDATE_ROOT"] ?? ""
if ($rustupMirror) { $env:RUSTUP_DIST_SERVER = $rustupMirror }
if ($rustupRoot) { $env:RUSTUP_UPDATE_ROOT = $rustupRoot }
# 先把 cargo\bin 加入 PATHrustup 通过 winget 安装后 PATH 需刷新)
$cargoBin = "$env:USERPROFILE\.cargo\bin"
if (Test-Path $cargoBin) { $env:Path = "$cargoBin;$env:Path" }
Refresh-Path
$cargoCmd = Get-Command cargo -ErrorAction SilentlyContinue
if ($cargoCmd) {
# 检查是否有默认 toolchain若没有则安装 stable
$rustcVer = & rustc --version 2>&1 | Out-String
if ($rustcVer -match "no default is configured" -or $rustcVer -match "error:") {
Write-Info "rustup 无默认 toolchain安装 stable..."
& rustup toolchain install stable 2>&1 | Select-Object -Last 3
& rustup default stable 2>&1 | Out-Null
Refresh-Path
if (Test-Path $cargoBin) { $env:Path = "$cargoBin;$env:Path" }
$rustcVer = & rustc --version 2>&1 | Out-String
}
Write-OK "Rust 已安装: $($rustcVer.Trim())"
} else {
# 检查 winget 是否已记录安装(避免重复安装时阻塞)
$wgPath = (Get-Command winget -ErrorAction SilentlyContinue)?.Source
if (-not $wgPath) {
$fallback = "$env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe"
if (Test-Path $fallback) { $wgPath = $fallback }
}
$wgList = if ($wgPath) { & $wgPath list --id Rustlang.Rustup --accept-source-agreements 2>&1 | Out-String } else { "" }
if ($wgList -match "Rustlang.Rustup") {
Write-Info "rustup 已安装,但当前会话 PATH 未更新。已将 $cargoBin 加入 PATH"
# rustup 首次运行会初始化 stable 工具链
if (Test-Path "$cargoBin\rustup.exe") { & "$cargoBin\rustup.exe" show 2>&1 | Out-Null }
$cargoCmd = Get-Command cargo -ErrorAction SilentlyContinue
if ($cargoCmd) { Write-OK "Rust: $(& rustc --version 2>&1)" }
} else {
Write-Info "通过 winget 安装 Rust (rustup)..."
$ok = Install-Winget "Rustlang.Rustup" "Rustup"
if ($ok) {
if (Test-Path $cargoBin) { $env:Path = "$cargoBin;$env:Path" }
Refresh-Path
$cargoCmd = Get-Command cargo -ErrorAction SilentlyContinue
if ($cargoCmd) {
Write-OK "Rust 安装完成: $(& rustc --version 2>&1)"
} else {
Write-Warn "Rust 安装后未找到 cargo请关闭并重开终端后继续"
Write-Warn " 或手动安装https://rustup.rs"
}
}
}
}
# 先把 ~/.local/bin 加入 PATH预编译包安装目录
$localBin = "$env:USERPROFILE\.local\bin"
if (Test-Path $localBin) { $env:Path = "$localBin;$env:Path" }
$rtkCmd = Get-Command rtk -ErrorAction SilentlyContinue
if ($rtkCmd) {
Write-OK "rtk 已安装: $(& rtk --version 2>&1)"
} else {
# 从 GitHub Releases 下载预编译包(比 cargo install 快得多,且无需编译环境)
Write-Info "安装 rtk (Rust Token Killer) - 下载预编译包..."
try {
$rtkVer = "v0.42.0"
$rtkUrl = "https://github.com/rtk-ai/rtk/releases/download/$rtkVer/rtk-x86_64-pc-windows-msvc.zip"
New-Item -ItemType Directory -Force -Path $localBin | Out-Null
$rtkZip = "$env:TEMP\rtk-windows.zip"
Invoke-WebRequest -Uri $rtkUrl -OutFile $rtkZip -UseBasicParsing
Expand-Archive -Path $rtkZip -DestinationPath $localBin -Force
Remove-Item $rtkZip -ErrorAction SilentlyContinue
$env:Path = "$localBin;$env:Path"
$rtkCmd = Get-Command rtk -ErrorAction SilentlyContinue
if ($rtkCmd) {
Write-OK "rtk 安装完成: $(& rtk --version 2>&1)"
} else {
Write-Warn "rtk 安装失败可手动运行cargo install --git https://github.com/rtk-ai/rtk"
}
} catch {
Write-Warn "rtk 下载失败: $_"
Write-Warn "可手动运行cargo install --git https://github.com/rtk-ai/rtk"
}
}
# ── rtk init -g注册 Claude Code PreToolUse hook ─────────────
if (Get-Command rtk -ErrorAction SilentlyContinue) {
Write-Info "注册 rtk hook 到 Claude Code..."
# 先禁用遥测,避免交互式确认提示阻塞管道
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
& rtk telemetry disable 2>&1 | Out-Null
try {
& rtk init -g --auto-patch 2>&1 | Out-Null
Write-OK "rtk hook 已注册(重启 Claude Code 后生效)"
} catch {
Write-Warn "rtk init 异常hook 可能已注册): $_"
} finally {
$ErrorActionPreference = $prevEAP
}
# ── rtk Copilot Instructions写入 .github/copilot-instructions.md ──
# 使用已有的(位于仓库根 .github/),仅在缺失时生成
$copilotDir = Join-Path $ScriptDir ".." ".github"
$copilotInst = Join-Path $copilotDir "copilot-instructions.md"
if (-not (Test-Path $copilotInst)) {
Write-Info "生成 .github/copilot-instructions.md ..."
if (-not (Test-Path $copilotDir)) { New-Item -ItemType Directory -Path $copilotDir | Out-Null }
& rtk init -g --copilot 2>&1 | Out-Null
Write-OK "Copilot Instructions 已生成"
} else {
Write-OK "Copilot Instructions 已存在,跳过生成"
}
}
# ══════════════════════════════════════════════════════════════
# 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 = @"
# Windows Dev Stack (generated by windows-dev-stack\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"
# rtk / cargo PATH PATH
`$_localBin = "`$env:USERPROFILE\.local\bin"
if ((Test-Path `$_localBin) -and `$env:Path -notlike "*.local\bin*") { `$env:Path = "`$_localBin;`$env:Path" }
`$_cargoBin = "`$env:USERPROFILE\.cargo\bin"
if ((Test-Path `$_cargoBin) -and `$env:Path -notlike "*cargo\bin*") { `$env:Path = "`$_cargoBin;`$env:Path" }
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 "Windows Dev Stack") {
# 注意:直接用 -replace 会将 $_ 等解释为正则反向引用导致内容爆炸
# 使用 MatchEvaluator scriptblock 让替换字符串按字面量处理
$blockCapture = $profileBlock
$existing = [regex]::Replace($existing, '(?ms)# ── Windows Dev Stack.*?# ─+', { $blockCapture })
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 "║ ✅ Windows 原生全栈部署完成 ║" -ForegroundColor Green
Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Green
Write-Host ""
$nodeVer = if (Get-Command node -ErrorAction SilentlyContinue) { & node --version 2>&1 } else { "(未安装)" }
$claudeVer = if (Get-Command claude -ErrorAction SilentlyContinue) { & claude --version 2>&1 | Select-Object -First 1 } else { "(未安装)" }
$ghVer = if (Get-Command gh -ErrorAction SilentlyContinue) { & gh --version 2>&1 | Select-Object -First 1 } else { "(未安装)" }
$rustVer = if (Get-Command rustc -ErrorAction SilentlyContinue) { & rustc --version 2>&1 } else { "(未安装)" }
$rtkVer = if (Get-Command rtk -ErrorAction SilentlyContinue) { & rtk --version 2>&1 } else { "(未安装)" }
$items = @(
@("Node.js", $nodeVer),
@("Claude Code", $claudeVer),
@("GitHub CLI", $ghVer),
@("Rust", $rustVer),
@("rtk", $rtkVer),
@("Unity MCP", $(if ($unityEntry) { $unityEntry } else { "(未安装)" })),
@("Godot MCP Pro", $(if ($godotEntry) { $godotEntry } else { "(未安装)" })),
@("Bridge Port", "TCP $BridgePort (防火墙已放行 $BridgePort-$($BridgePort+9))")
)
foreach ($item in $items) {
$label = $item[0].PadRight(14)
Write-Host " $label $($item[1])" -ForegroundColor White
}
Write-Host ""
Write-Host " 后续手动步骤:" -ForegroundColor Yellow
Write-Host " 1. gh auth login # GitHub CLI 登录(首次)" -ForegroundColor DarkGray
Write-Host " 2. Unity Editor 安装 unity-mcp-plugin" -ForegroundColor DarkGray
Write-Host " https://github.com/AnkleBreaker-Studio/unity-mcp-plugin.git" -ForegroundColor DarkGray
Write-Host " 3. Godot 项目:复制 addons/godot_mcp/ 并在 Plugins 中启用" -ForegroundColor DarkGray
Write-Host " 源:$(Join-Path $godotPkgBase "$GodotMcpVersion\addons\godot_mcp")" -ForegroundColor DarkGray
Write-Host " 4. 重启 AI 客户端Claude Desktop / Cursor / Windsurf / VS Code" -ForegroundColor DarkGray
Write-Host ""