Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions Editor/Resources/CreateSceneResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using System;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;
using UnityEditor;
using Newtonsoft.Json.Linq;
using McpUnity.Utils;

namespace McpUnity.Resources
{
/// <summary>
/// Resource for creating a new Unity scene
/// </summary>
public class CreateSceneResource : McpResourceBase
{
public CreateSceneResource()
{
Name = "create_scene";
Description = "Creates a new scene and saves it to the specified path";
Uri = "unity://scenes/create";
}

/// <summary>
/// Create a new scene with the provided parameters
/// </summary>
/// <param name="parameters">Resource parameters as a JObject</param>
/// <returns>A JObject containing the result</returns>
public override JObject Fetch(JObject parameters)
{
// Parameters
string sceneName = parameters["sceneName"]?.ToObject<string>();
string folderPath = parameters["folderPath"]?.ToObject<string>();
bool addToBuildSettings = parameters["addToBuildSettings"]?.ToObject<bool?>() ?? false;
bool makeActive = parameters["makeActive"]?.ToObject<bool?>() ?? true;

if (string.IsNullOrEmpty(sceneName))
{
return new JObject
{
["success"] = false,
["message"] = "Required parameter 'sceneName' not provided",
["error"] = new JObject
{
["type"] = "validation_error",
["message"] = "Required parameter 'sceneName' not provided"
}
};
}

// Default folder path
if (string.IsNullOrEmpty(folderPath))
{
folderPath = "Assets";
}

// Ensure folder exists
if (!AssetDatabase.IsValidFolder(folderPath))
{
string[] parts = folderPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
string current = parts.Length > 0 && parts[0] == "Assets" ? "Assets" : "Assets";
for (int i = 0; i < parts.Length; i++)
{
if (i == 0 && parts[i] == "Assets") continue;
string next = current + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next))
{
AssetDatabase.CreateFolder(current, parts[i]);
}
current = next;
}
}

// Create unique path for the scene
string basePath = folderPath.TrimEnd('/');
string scenePath = AssetDatabase.GenerateUniqueAssetPath($"{basePath}/{sceneName}.unity");

try
{
var newScene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);

bool saved = EditorSceneManager.SaveScene(newScene, scenePath);
if (!saved)
{
return new JObject
{
["success"] = false,
["message"] = $"Failed to save scene at '{scenePath}'",
["error"] = new JObject
{
["type"] = "save_error",
["message"] = $"Failed to save scene at '{scenePath}'"
}
};
}

AssetDatabase.Refresh();

// Make the scene active if requested
if (makeActive)
{
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
}

// Optionally add to build settings
if (addToBuildSettings)
{
AddSceneToBuildSettings(scenePath);
}

McpLogger.LogInfo($"Created scene '{sceneName}' at path '{scenePath}'");

return new JObject
{
["success"] = true,
["message"] = $"Successfully created scene '{sceneName}' at path '{scenePath}'",
["scenePath"] = scenePath
};
}
catch (Exception ex)
{
return new JObject
{
["success"] = false,
["message"] = $"Error creating scene: {ex.Message}",
["error"] = new JObject
{
["type"] = "scene_creation_error",
["message"] = $"Error creating scene: {ex.Message}"
}
};
}
}

private void AddSceneToBuildSettings(string scenePath)
{
var scenes = EditorBuildSettings.scenes;

foreach (var s in scenes)
{
if (s.path == scenePath)
{
return;
}
}

var newList = new EditorBuildSettingsScene[scenes.Length + 1];
for (int i = 0; i < scenes.Length; i++)
{
newList[i] = scenes[i];
}
newList[newList.Length - 1] = new EditorBuildSettingsScene(scenePath, true);
EditorBuildSettings.scenes = newList;
}
}
}
84 changes: 84 additions & 0 deletions Editor/Resources/GetSceneInfoResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using Newtonsoft.Json.Linq;
using McpUnity.Utils;

namespace McpUnity.Resources
{
/// <summary>
/// Resource for getting information about the active scene
/// </summary>
public class GetSceneInfoResource : McpResourceBase
{
public GetSceneInfoResource()
{
Name = "get_scene_info";
Description = "Gets information about the active scene and all loaded scenes";
Uri = "unity://scene/info";
}

/// <summary>
/// Fetch scene information
/// </summary>
/// <param name="parameters">Resource parameters as a JObject</param>
/// <returns>A JObject containing scene information</returns>
public override JObject Fetch(JObject parameters)
{
try
{
var sceneInfo = new JObject();
var activeScene = SceneManager.GetActiveScene();

sceneInfo["activeScene"] = new JObject
{
["name"] = activeScene.name,
["path"] = activeScene.path,
["buildIndex"] = activeScene.buildIndex,
["isLoaded"] = activeScene.isLoaded,
["isDirty"] = activeScene.isDirty,
["rootObjectCount"] = activeScene.GetRootGameObjects()?.Length ?? 0
};

var loadedScenes = new JArray();
int sceneCount = SceneManager.sceneCount;
for (int i = 0; i < sceneCount; i++)
{
Scene scene = SceneManager.GetSceneAt(i);
loadedScenes.Add(new JObject
{
["name"] = scene.name,
["path"] = scene.path,
["buildIndex"] = scene.buildIndex,
["isLoaded"] = scene.isLoaded,
["isDirty"] = scene.isDirty
});
}

sceneInfo["loadedScenes"] = loadedScenes;
sceneInfo["loadedSceneCount"] = sceneCount;

return new JObject
{
["success"] = true,
["message"] = "Successfully retrieved scene info",
["sceneInfo"] = sceneInfo
};
}
catch (Exception ex)
{
return new JObject
{
["success"] = false,
["message"] = $"Error getting scene info: {ex.Message}",
["error"] = new JObject
{
["type"] = "scene_info_error",
["message"] = $"Error getting scene info: {ex.Message}"
}
};
}
}
}
}
50 changes: 39 additions & 11 deletions Editor/Services/ConsoleLogsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ private class LogEntry
public DateTime Timestamp { get; set; }
}

// Reflection cache for Unity internal API (avoids repeated reflection)
private static MethodInfo _logEntriesGetCountMethod;
private static bool _reflectionInitialized;
private static readonly object _reflectionLock = new object();

// Constants for log management
private const int MaxLogEntries = 1000;
private const int CleanupThreshold = 200; // Remove oldest entries when exceeding max
Expand Down Expand Up @@ -201,20 +206,41 @@ public int GetLogCount()

/// <summary>
/// Check if console was cleared using reflection (for Unity 2022.3)
/// Uses cached reflection to avoid repeated expensive reflection calls
/// </summary>
private void CheckConsoleClearViaReflection()
{
// Initialize reflection once
if (!_reflectionInitialized)
{
lock (_reflectionLock)
{
if (!_reflectionInitialized)
{
try
{
var logEntriesType = Type.GetType("UnityEditor.LogEntries,UnityEditor");
if (logEntriesType != null)
{
_logEntriesGetCountMethod = logEntriesType.GetMethod("GetCount",
BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic);
}
}
catch (Exception ex)
{
Debug.LogWarning($"[MCP Unity] Failed to initialize LogEntries reflection: {ex.Message}");
}
_reflectionInitialized = true;
}
}
}

// If reflection failed to initialize, skip this check
if (_logEntriesGetCountMethod == null) return;

try
{
// Get current log counts using LogEntries (internal Unity API)
var logEntriesType = Type.GetType("UnityEditor.LogEntries,UnityEditor");
if (logEntriesType == null) return;

var getCountMethod = logEntriesType.GetMethod("GetCount",
BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic);
if (getCountMethod == null) return;

int currentTotalCount = (int)getCountMethod.Invoke(null, null);
int currentTotalCount = (int)_logEntriesGetCountMethod.Invoke(null, null);

// If we had logs before, but now we don't, console was likely cleared
if (currentTotalCount == 0 && _logEntries.Count > 0)
Expand All @@ -224,8 +250,10 @@ private void CheckConsoleClearViaReflection()
}
catch (Exception ex)
{
// Just log the error but don't break functionality
Debug.LogError($"[MCP Unity] Error checking console clear: {ex.Message}");
// Reflection invocation failed - clear cache so it retries next time
_logEntriesGetCountMethod = null;
_reflectionInitialized = false;
Debug.LogWarning($"[MCP Unity] LogEntries reflection failed, will retry: {ex.Message}");
}
}

Expand Down
30 changes: 29 additions & 1 deletion Editor/Services/TestRunnerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ namespace McpUnity.Services
/// Service for accessing Unity Test Runner functionality
/// Implements ICallbacks for TestRunnerApi.
/// </summary>
public class TestRunnerService : ITestRunnerService, ICallbacks
public class TestRunnerService : ITestRunnerService, ICallbacks, IDisposable
{
private readonly TestRunnerApi _testRunnerApi;
private TaskCompletionSource<JObject> _tcs;
private bool _returnOnlyFailures;
private bool _returnWithLogs;
private List<ITestResultAdaptor> _results;
private bool _disposed;

/// <summary>
/// Constructor
Expand All @@ -34,6 +35,33 @@ public TestRunnerService()
_testRunnerApi.RegisterCallbacks(this);
}

/// <summary>
/// Unregisters callbacks to prevent memory leaks
/// </summary>
public void Dispose()
{
if (!_disposed)
{
try
{
_testRunnerApi?.UnregisterCallbacks(this);
}
catch (Exception ex)
{
McpLogger.LogWarning($"[MCP Unity] Error unregistering TestRunner callbacks: {ex.Message}");
}
_disposed = true;
}
}

/// <summary>
/// Finalizer to ensure cleanup
/// </summary>
~TestRunnerService()
{
Dispose();
}

/// <summary>
/// Async retrieval of all tests using TestRunnerApi callbacks
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions Editor/Tools/BatchExecuteTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ namespace McpUnity.Tools
/// Tool for executing multiple operations in a single batch request.
/// Supports sequential execution, stop-on-error, and atomic rollback.
/// </summary>
/// <remarks>
/// <para><b>Important: Atomic rollback limitation</b></para>
/// <para>The <c>atomic</c> parameter uses Unity's <c>Undo</c> system for rollback, which only works in the Unity Editor.</para>
/// <para>In Play Mode, builds, or headless/batch mode, the Undo system is not available and atomic rollback will NOT work.</para>
/// <para>When <c>atomic=true</c> is used outside the Editor, operations will still execute sequentially with stop-on-error,</para>
/// <para>but no rollback will occur on failure. Consider this when designing automated workflows.</para>
/// </remarks>
public class BatchExecuteTool : McpToolBase
{
private readonly Func<string, McpToolBase> _getTool;
Expand Down
8 changes: 8 additions & 0 deletions Editor/UnityBridge/McpUnityServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,14 @@ private void RegisterResources()
GetTestsResource getTestsResource = new GetTestsResource(_testRunnerService);
_resources.Add(getTestsResource.Name, getTestsResource);

// Register GetSceneInfoResource
GetSceneInfoResource getSceneInfoResource = new GetSceneInfoResource();
_resources.Add(getSceneInfoResource.Name, getSceneInfoResource);

// Register CreateSceneResource
CreateSceneResource createSceneResource = new CreateSceneResource();
_resources.Add(createSceneResource.Name, createSceneResource);

// Register GetGameObjectResource
GetGameObjectResource getGameObjectResource = new GetGameObjectResource();
_resources.Add(getGameObjectResource.Name, getGameObjectResource);
Expand Down
Loading