using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Tools.Build; namespace MCPForUnity.Editor.Tools { [McpForUnityTool("manage_build", AutoRegister = false, Group = "core", RequiresPolling = true, PollAction = "status", MaxPollSeconds = 1800)] public static class ManageBuild { private static readonly string[] ValidActions = { "build", "status", "platform", "settings", "scenes", "profiles", "batch", "cancel" }; public static object HandleCommand(JObject @params) { if (@params == null) return new ErrorResponse("Parameters cannot be null."); var p = new ToolParams(@params); var actionResult = p.GetRequired("action"); if (!actionResult.IsSuccess) return new ErrorResponse(actionResult.ErrorMessage); string action = actionResult.Value.ToLowerInvariant(); if (!ValidActions.Contains(action)) return new ErrorResponse( $"Unknown action '{action}'. Valid actions: {string.Join(", ", ValidActions)}"); try { switch (action) { case "build": return HandleBuild(p); case "status": return HandleStatus(p); case "platform": return HandlePlatform(p); case "settings": return HandleSettings(p); case "scenes": return HandleScenes(p); case "profiles": return HandleProfiles(p); case "batch": return HandleBatch(p); case "cancel": return HandleCancel(p); default: return new ErrorResponse($"Unknown action: '{action}'"); } } catch (Exception ex) { return new ErrorResponse(ex.Message, new { stackTrace = ex.StackTrace }); } } // ── build ────────────────────────────────────────────────────── private static object HandleBuild(ToolParams p) { if (BuildPipeline.isBuildingPlayer) return new ErrorResponse("A build is already in progress."); string targetName = p.Get("target"); if (!BuildTargetMapping.TryResolveBuildTarget(targetName, out var target)) return new ErrorResponse(BuildTargetMapping.GetUnknownBuildTargetMessage(targetName)); var group = BuildTargetMapping.GetTargetGroup(target); if (!BuildPipeline.IsBuildTargetSupported(group, target)) return new ErrorResponse( $"Platform '{target}' is not installed. Install it via Unity Hub."); string outputPath = p.Get("output_path") ?? BuildTargetMapping.GetDefaultOutputPath(target, PlayerSettings.productName); string[] scenes = p.GetStringArray("scenes"); bool development = p.GetBool("development"); string[] optionNames = p.GetStringArray("options"); string subtargetStr = p.Get("subtarget"); string scriptingBackend = p.Get("scripting_backend"); // Apply scripting backend if specified (persistent change) if (!string.IsNullOrEmpty(scriptingBackend)) { string backendLower = scriptingBackend.ToLowerInvariant(); if (backendLower != "il2cpp" && backendLower != "mono") return new ErrorResponse( $"Unknown scripting_backend '{scriptingBackend}'. Valid: mono, il2cpp"); var namedTarget = BuildTargetMapping.GetNamedBuildTarget(target); var impl = backendLower == "il2cpp" ? ScriptingImplementation.IL2CPP : ScriptingImplementation.Mono2x; PlayerSettings.SetScriptingBackend(namedTarget, impl); } #if UNITY_6000_0_OR_NEWER string profilePath = p.Get("profile"); if (!string.IsNullOrEmpty(profilePath)) return HandleProfileBuild(p, profilePath, outputPath, development, optionNames); #else string profilePath = p.Get("profile"); if (!string.IsNullOrEmpty(profilePath)) McpLog.Warn($"Build Profile param ignored — requires Unity 6+. Current: {UnityEngine.Application.unityVersion}"); #endif var buildOptions = BuildRunner.ParseBuildOptions(optionNames, development); int subtarget = BuildTargetMapping.ResolveSubtarget(subtargetStr); var options = BuildRunner.CreateBuildOptions(target, outputPath, scenes, buildOptions, subtarget); string jobId = BuildJobStore.CreateJobId(); var job = new BuildJob(jobId, target, outputPath); return BuildRunner.ScheduleBuild(job, options); } #if UNITY_6000_0_OR_NEWER private static object HandleProfileBuild(ToolParams p, string profilePath, string outputPath, bool development, string[] optionNames) { var profile = UnityEditor.AssetDatabase.LoadAssetAtPath< UnityEditor.Build.Profile.BuildProfile>(profilePath); if (profile == null) return new ErrorResponse($"Build profile not found at: {profilePath}"); var buildOptions = BuildRunner.ParseBuildOptions(optionNames, development); var options = new BuildPlayerWithProfileOptions { buildProfile = profile, locationPathName = outputPath, options = buildOptions }; // BuildPlayerWithProfileOptions derives the actual target from the profile, // but we use activeBuildTarget for job metadata/status display var target = EditorUserBuildSettings.activeBuildTarget; string jobId = BuildJobStore.CreateJobId(); var job = new BuildJob(jobId, target, outputPath); return BuildRunner.ScheduleProfileBuild(job, options); } #endif // ── status ───────────────────────────────────────────────────── private static object HandleStatus(ToolParams p) { string jobId = p.Get("job_id"); if (string.IsNullOrEmpty(jobId)) { // Prefer active (pending/building) job — needed for polling middleware var last = BuildJobStore.LastCompletedJob; if (last != null && (last.State == BuildJobState.Building || last.State == BuildJobState.Pending)) { return new PendingResponse( $"Build {last.State.ToString().ToLowerInvariant()}...", pollIntervalSeconds: 5.0, data: last.ToStatusResponse() ); } if (last != null) return new SuccessResponse("Last completed build.", last.ToStatusResponse()); #if UNITY_6000_0_OR_NEWER var latestReport = BuildReport.GetLatestReport(); if (latestReport != null) { var s = latestReport.summary; return new SuccessResponse("Last build report from Unity.", new { result = s.result.ToString().ToLowerInvariant(), platform = s.platform.ToString(), output_path = s.outputPath, total_size_mb = Math.Round(s.totalSize / (1024.0 * 1024.0), 2), duration_seconds = s.totalTime.TotalSeconds, errors = s.totalErrors, warnings = s.totalWarnings }); } #endif return new ErrorResponse("No build jobs found."); } // Check batch jobs first var batchJob = BuildJobStore.GetBatchJob(jobId); if (batchJob != null) return new SuccessResponse($"Batch {batchJob.State}.", batchJob.ToStatusResponse()); var buildJob = BuildJobStore.GetBuildJob(jobId); if (buildJob == null) return new ErrorResponse($"No job found with ID: {jobId}"); if (buildJob.State == BuildJobState.Building || buildJob.State == BuildJobState.Pending) { return new PendingResponse( $"Build {buildJob.State.ToString().ToLowerInvariant()}...", pollIntervalSeconds: 5.0, data: buildJob.ToStatusResponse() ); } return new SuccessResponse($"Build {buildJob.State}.", buildJob.ToStatusResponse()); } // ── platform ─────────────────────────────────────────────────── private static object HandlePlatform(ToolParams p) { string targetName = p.Get("target"); if (string.IsNullOrEmpty(targetName)) { // Read current platform return new SuccessResponse("Current platform.", new { target = EditorUserBuildSettings.activeBuildTarget.ToString(), target_group = BuildTargetMapping.GetTargetGroup( EditorUserBuildSettings.activeBuildTarget).ToString(), subtarget = EditorUserBuildSettings.standaloneBuildSubtarget.ToString() }); } // Switch platform if (!BuildTargetMapping.TryResolveBuildTarget(targetName, out var target)) return new ErrorResponse(BuildTargetMapping.GetUnknownBuildTargetMessage(targetName)); var group = BuildTargetMapping.GetTargetGroup(target); if (!BuildPipeline.IsBuildTargetSupported(group, target)) return new ErrorResponse( $"Platform '{target}' is not installed. Install it via Unity Hub."); if (EditorUserBuildSettings.activeBuildTarget == target) return new SuccessResponse("Already on this platform.", new { target = target.ToString() }); // Capture previous target before switching string previousTarget = EditorUserBuildSettings.activeBuildTarget.ToString(); string subtargetStr = p.Get("subtarget"); if (!string.IsNullOrEmpty(subtargetStr)) { string subtargetLower = subtargetStr.ToLowerInvariant(); if (subtargetLower == "server") EditorUserBuildSettings.standaloneBuildSubtarget = StandaloneBuildSubtarget.Server; else if (subtargetLower == "player") EditorUserBuildSettings.standaloneBuildSubtarget = StandaloneBuildSubtarget.Player; } // SwitchActiveBuildTarget is synchronous — blocks until reimport completes EditorUserBuildSettings.SwitchActiveBuildTarget(group, target); return new SuccessResponse( $"Switched to {target}. Assets reimported for new platform.", new { target = target.ToString(), previous = previousTarget } ); } // ── settings ─────────────────────────────────────────────────── private static object HandleSettings(ToolParams p) { var propertyResult = p.GetRequired("property"); if (!propertyResult.IsSuccess) return new ErrorResponse(propertyResult.ErrorMessage); string property = propertyResult.Value.ToLowerInvariant(); string targetName = p.Get("target"); string value = p.Get("value"); // Resolve target string err = BuildTargetMapping.TryResolveNamedBuildTarget(targetName, out var namedTarget); if (err != null) return new ErrorResponse(err); if (string.IsNullOrEmpty(value)) { // Read var result = BuildSettingsHelper.ReadProperty(property, namedTarget); if (result == null) return new ErrorResponse( $"Unknown property '{property}'. Valid: {string.Join(", ", BuildSettingsHelper.ValidProperties)}"); return new SuccessResponse($"Read {property}.", result); } // Write string writeErr = BuildSettingsHelper.WriteProperty(property, value, namedTarget); if (writeErr != null) return new ErrorResponse(writeErr); return new SuccessResponse($"Set {property} = {value}.", BuildSettingsHelper.ReadProperty(property, namedTarget)); } // ── scenes ───────────────────────────────────────────────────── private static object HandleScenes(ToolParams p) { var scenesRaw = p.GetRaw("scenes"); if (scenesRaw == null || scenesRaw.Type == JTokenType.Null) { // Read current scene list var scenes = EditorBuildSettings.scenes.Select(s => new { path = s.path, enabled = s.enabled, guid = s.guid.ToString() }).ToArray(); return new SuccessResponse($"Build scenes ({scenes.Length}).", new { scenes }); } // Handle string input: try JSON parse, then comma-separated if (scenesRaw.Type == JTokenType.String) { string scenesStr = scenesRaw.ToString(); try { scenesRaw = JArray.Parse(scenesStr); } catch { // Treat as comma-separated paths var paths = scenesStr.Split(',') .Select(s => s.Trim()) .Where(s => !string.IsNullOrEmpty(s)) .ToArray(); if (paths.Length == 0) return new ErrorResponse("'scenes' string contained no valid paths."); var fromStr = paths.Select(sp => new EditorBuildSettingsScene(sp, true)).ToArray(); EditorBuildSettings.scenes = fromStr; return new SuccessResponse($"Updated build scenes ({fromStr.Length}).", new { scenes = fromStr.Select(s => new { path = s.path, enabled = s.enabled }).ToArray() }); } } // Write scene list — accepts array of strings or array of {path, enabled} objects var sceneArray = scenesRaw as JArray; if (sceneArray == null) return new ErrorResponse("'scenes' must be an array of scene paths or {path, enabled} objects."); var newScenes = new List(); foreach (var item in sceneArray) { if (item.Type == JTokenType.String) { newScenes.Add(new EditorBuildSettingsScene(item.ToString(), true)); continue; } string path = item["path"]?.ToString(); if (string.IsNullOrEmpty(path)) return new ErrorResponse("Each scene must have a 'path' field."); bool enabled = item["enabled"]?.Value() ?? true; newScenes.Add(new EditorBuildSettingsScene(path, enabled)); } EditorBuildSettings.scenes = newScenes.ToArray(); return new SuccessResponse($"Updated build scenes ({newScenes.Count}).", new { scenes = newScenes.Select(s => new { path = s.path, enabled = s.enabled }).ToArray() }); } // ── profiles ─────────────────────────────────────────────────── private static object HandleProfiles(ToolParams p) { #if UNITY_6000_0_OR_NEWER string profilePath = p.Get("profile"); bool activate = p.GetBool("activate"); if (string.IsNullOrEmpty(profilePath)) { // List all profiles var guids = AssetDatabase.FindAssets("t:BuildProfile"); var profiles = guids.Select(guid => { string path = AssetDatabase.GUIDToAssetPath(guid); return new { path, name = System.IO.Path.GetFileNameWithoutExtension(path) }; }).ToArray(); var active = UnityEditor.Build.Profile.BuildProfile.GetActiveBuildProfile(); return new SuccessResponse($"Found {profiles.Length} build profiles.", new { profiles, active_profile = active != null ? AssetDatabase.GetAssetPath(active) : null }); } var loadedProfile = AssetDatabase.LoadAssetAtPath< UnityEditor.Build.Profile.BuildProfile>(profilePath); if (loadedProfile == null) return new ErrorResponse($"Build profile not found at: {profilePath}"); if (activate) { UnityEditor.Build.Profile.BuildProfile.SetActiveBuildProfile(loadedProfile); return new SuccessResponse($"Activated build profile: {profilePath}", new { profile = profilePath, activated = true }); } // Get profile details — use .scenes (available since Unity 6000.0.0) // instead of .GetScenesForBuild() which was added in 6000.0.36 var profileScenes = loadedProfile.scenes .Select(s => s.path).ToArray(); return new SuccessResponse($"Profile: {profilePath}", new { profile = profilePath, scenes = profileScenes }); #else string version = UnityEngine.Application.unityVersion; return new ErrorResponse( $"Build Profiles require Unity 6 (6000.0+). Current version: {version}"); #endif } // ── batch ────────────────────────────────────────────────────── private static object HandleBatch(ToolParams p) { if (BuildPipeline.isBuildingPlayer) return new ErrorResponse("A build is already in progress."); string[] targets = p.GetStringArray("targets"); string[] profiles = p.GetStringArray("profiles"); string outputDir = p.Get("output_dir") ?? "Builds"; bool development = p.GetBool("development"); string[] optionNames = p.GetStringArray("options"); if ((targets == null || targets.Length == 0) && (profiles == null || profiles.Length == 0)) return new ErrorResponse("'targets' or 'profiles' is required for batch builds."); if (targets != null && targets.Length > 0 && profiles != null && profiles.Length > 0) return new ErrorResponse("Provide 'targets' or 'profiles', not both."); // Validate all targets/profiles upfront before creating store entries if (targets != null && targets.Length > 0) { var resolvedTargets = new List<(BuildTarget bt, string path)>(); foreach (var t in targets) { if (!BuildTargetMapping.TryResolveBuildTarget(t, out var bt)) return new ErrorResponse(BuildTargetMapping.GetUnknownBuildTargetMessage(t)); var btGroup = BuildTargetMapping.GetTargetGroup(bt); if (!BuildPipeline.IsBuildTargetSupported(btGroup, bt)) return new ErrorResponse( $"Platform '{bt}' is not installed. Install it via Unity Hub."); string defaultPath = BuildTargetMapping.GetDefaultOutputPath(bt, PlayerSettings.productName); string path = defaultPath.StartsWith("Builds/") ? $"{outputDir}/{defaultPath.Substring(7)}" : $"{outputDir}/{defaultPath}"; resolvedTargets.Add((bt, path)); } string batchId = BuildJobStore.CreateBatchId(); var batch = new BatchJob(batchId); batch.State = BuildJobState.Building; BuildJobStore.AddBatchJob(batch); foreach (var (bt, path) in resolvedTargets) { var child = new BuildJob(BuildJobStore.CreateJobId(), bt, path); batch.Children.Add(child); } var buildOpts = BuildRunner.ParseBuildOptions(optionNames, development); BuildRunner.ScheduleNextBatchBuild(batch, index => { var child = batch.Children[index]; var group = BuildTargetMapping.GetTargetGroup(child.Target); // Platform switch is required — ensures correct shader variants, // asset import settings, and scripting defines for the target if (EditorUserBuildSettings.activeBuildTarget != child.Target) EditorUserBuildSettings.SwitchActiveBuildTarget(group, child.Target); int subtarget = (int)StandaloneBuildSubtarget.Player; var options = BuildRunner.CreateBuildOptions( child.Target, child.OutputPath, null, buildOpts, subtarget); BuildRunner.ScheduleBuild(child, options); return child; }); return new PendingResponse( $"Batch build started ({batch.Children.Count} builds).", pollIntervalSeconds: 10.0, data: new { job_id = batchId, total = batch.Children.Count } ); } #if UNITY_6000_0_OR_NEWER if (profiles != null && profiles.Length > 0) { // Validate all profiles exist before creating any store entries var loadedProfiles = new List(); foreach (var profilePath in profiles) { var profile = AssetDatabase.LoadAssetAtPath< UnityEditor.Build.Profile.BuildProfile>(profilePath); if (profile == null) return new ErrorResponse($"Profile not found: {profilePath}"); loadedProfiles.Add(profile); } string batchId = BuildJobStore.CreateBatchId(); var batch = new BatchJob(batchId); batch.State = BuildJobState.Building; BuildJobStore.AddBatchJob(batch); for (int i = 0; i < profiles.Length; i++) { var target = EditorUserBuildSettings.activeBuildTarget; string name = System.IO.Path.GetFileNameWithoutExtension(profiles[i]); string path = $"{outputDir}/{name}/{PlayerSettings.productName}"; var child = new BuildJob(BuildJobStore.CreateJobId(), target, path); batch.Children.Add(child); } var buildOpts = BuildRunner.ParseBuildOptions(optionNames, development); BuildRunner.ScheduleNextBatchBuild(batch, index => { var child = batch.Children[index]; var opts = new BuildPlayerWithProfileOptions { buildProfile = loadedProfiles[index], locationPathName = child.OutputPath, options = buildOpts }; BuildRunner.ScheduleProfileBuild(child, opts); return child; }); return new PendingResponse( $"Batch build started ({batch.Children.Count} builds).", pollIntervalSeconds: 10.0, data: new { job_id = batchId, total = batch.Children.Count } ); } #else if (profiles != null && profiles.Length > 0) { return new ErrorResponse( $"Profile-based batch requires Unity 6+. Current: {UnityEngine.Application.unityVersion}"); } #endif return new ErrorResponse("'targets' or 'profiles' is required for batch builds."); } // ── cancel ───────────────────────────────────────────────────── private static object HandleCancel(ToolParams p) { var jobIdResult = p.GetRequired("job_id"); if (!jobIdResult.IsSuccess) return new ErrorResponse(jobIdResult.ErrorMessage); string jobId = jobIdResult.Value; var batchJob = BuildJobStore.GetBatchJob(jobId); if (batchJob != null) { if (batchJob.State == BuildJobState.Building) { batchJob.State = BuildJobState.Cancelled; return new SuccessResponse( "Batch cancelled. The current build will finish but no more builds will start.", new { job_id = jobId, state = "cancelled" }); } return new ErrorResponse($"Batch is already {batchJob.State}."); } var buildJob = BuildJobStore.GetBuildJob(jobId); if (buildJob != null) { if (buildJob.State == BuildJobState.Building) return new ErrorResponse( "Cannot cancel a single build in progress. BuildPipeline.BuildPlayer is " + "synchronous and blocks the editor until completion."); return new ErrorResponse($"Build is already {buildJob.State}."); } return new ErrorResponse($"No job found with ID: {jobId}"); } } }