diff --git a/.gitignore b/.gitignore index 0d55340..609ecbc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,12 @@ **/[Uu]ser[Ss]ettings/ **/[Oo]bj/Debug/ +# Development logs and scripts +log.txt +log.txt.meta +**/run_mcp_server_with_logging.sh +**/run_mcp_server_with_logging.sh.meta + # Asset meta data should only be ignored when the corresponding asset is also ignored !**/[Aa]ssets/**/*.meta diff --git a/Editor/Services/TestRunnerService.cs b/Editor/Services/TestRunnerService.cs index 7b7fc87..39c438f 100644 --- a/Editor/Services/TestRunnerService.cs +++ b/Editor/Services/TestRunnerService.cs @@ -55,7 +55,7 @@ public async void ExecuteTests( TestMode testMode, string testFilter, TaskCompletionSource completionSource, - int timeoutMinutes = 1) + int timeoutMinutes = 10) { // Create filter var filter = new Filter diff --git a/Editor/Tests.meta b/Editor/Tests.meta new file mode 100644 index 0000000..2bd6f09 --- /dev/null +++ b/Editor/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ac962b0e8c3334248828f0bad3d32ed7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Tools/AddAssetToSceneTool.cs b/Editor/Tools/AddAssetToSceneTool.cs index b08ea06..f48a42d 100644 --- a/Editor/Tools/AddAssetToSceneTool.cs +++ b/Editor/Tools/AddAssetToSceneTool.cs @@ -113,7 +113,12 @@ public override JObject Execute(JObject parameters) } else { - Debug.LogWarning($"[MCP Unity] Parent object not found, asset will be created at the root of the scene"); + // Parent not found, return an error instead of adding to root + string identifier = parentId.HasValue ? $"ID {parentId.Value}" : $"path '{parentPath}'"; + return McpUnitySocketHandler.CreateErrorResponse( + $"Parent GameObject not found with {identifier}", + "parent_not_found" + ); } } diff --git a/Editor/Tools/NotifyMessageTool.cs b/Editor/Tools/NotifyMessageTool.cs index cd9b93e..52cefd9 100644 --- a/Editor/Tools/NotifyMessageTool.cs +++ b/Editor/Tools/NotifyMessageTool.cs @@ -1,15 +1,27 @@ +using System; using System.Threading.Tasks; using McpUnity.Unity; using UnityEngine; +using UnityEditor; using Newtonsoft.Json.Linq; namespace McpUnity.Tools { /// - /// Tool for sending notification messages to the Unity console + /// Tool for displaying messages in the Unity console /// public class NotifyMessageTool : McpToolBase { + /// + /// Supported message types for Unity console output + /// + private enum MessageType + { + Info, + Warning, + Error + } + public NotifyMessageTool() { Name = "notify_message"; @@ -25,35 +37,45 @@ public override JObject Execute(JObject parameters) // Extract parameters string message = parameters["message"]?.ToObject(); string type = parameters["type"]?.ToObject()?.ToLower() ?? "info"; - + + // Validate message if (string.IsNullOrEmpty(message)) { return McpUnitySocketHandler.CreateErrorResponse( - "Required parameter 'message' not provided", + "Required parameter 'message' not provided or is empty", + "validation_error" + ); + } + + // Parse and validate message type + if (!Enum.TryParse(type, true, out var messageType)) + { + return McpUnitySocketHandler.CreateErrorResponse( + $"Invalid message type '{type}'. Valid types are: info, warning, error", "validation_error" ); } - + // Log the message based on type - switch (type) + switch (messageType) { - case "error": - Debug.LogError($"[MCP]: {message}"); + case MessageType.Warning: + Debug.LogWarning($"[MCP Unity] {message}"); break; - case "warning": - Debug.LogWarning($"[MCP]: {message}"); + case MessageType.Error: + Debug.LogError($"[MCP Unity] {message}"); break; default: - Debug.Log($"[MCP]: {message}"); + Debug.Log($"[MCP Unity] {message}"); break; } - + // Create the response return new JObject { ["success"] = true, - ["type"] = "text", - ["message"] = $"Message displayed: {message}" + ["message"] = $"Message displayed: {message}", + ["type"] = "text" }; } } diff --git a/Editor/Tools/RunTestsTool.cs b/Editor/Tools/RunTestsTool.cs index 97c6ef9..dc7041b 100644 --- a/Editor/Tools/RunTestsTool.cs +++ b/Editor/Tools/RunTestsTool.cs @@ -1,12 +1,12 @@ using System; -using System.Threading; +using System.Collections.Generic; using System.Threading.Tasks; using McpUnity.Unity; +using McpUnity.Services; +using UnityEditor; +using UnityEditor.TestTools.TestRunner.Api; using UnityEngine; using Newtonsoft.Json.Linq; -using System.Collections.Generic; -using UnityEditor.TestTools.TestRunner.Api; -using McpUnity.Services; namespace McpUnity.Tools { @@ -16,169 +16,148 @@ namespace McpUnity.Tools public class RunTestsTool : McpToolBase, ICallbacks { private readonly ITestRunnerService _testRunnerService; - - private bool _isRunning = false; - private TaskCompletionSource _testRunCompletionSource; - private List _testResults = new List(); - - // Structure to store test results - private class TestResult - { - public string Name { get; set; } - public string FullName { get; set; } - public string ResultState { get; set; } - public string Message { get; set; } - public double Duration { get; set; } - public bool Passed => ResultState == "Passed"; - } - + private TaskCompletionSource _testCompletionSource; + private List _testResults; + private int _testCount; + private int _passCount; + private int _failCount; + public RunTestsTool(ITestRunnerService testRunnerService) { Name = "run_tests"; - Description = "Runs tests using Unity's Test Runner"; + Description = "Runs Unity's Test Runner tests"; IsAsync = true; _testRunnerService = testRunnerService; - - // Register callbacks with the TestRunnerApi _testRunnerService.TestRunnerApi.RegisterCallbacks(this); + _testResults = new List(); + } + + public override JObject Execute(JObject parameters) + { + throw new NotSupportedException("This tool only supports async execution. Please use ExecuteAsync instead."); } - - /// - /// Executes the RunTests tool asynchronously on the main thread. - /// - /// Tool parameters, including optional 'testMode' and 'testFilter'. - /// TaskCompletionSource to set the result or exception. + public override void ExecuteAsync(JObject parameters, TaskCompletionSource tcs) { - // Check if tests are already running - if (_isRunning) + _testCompletionSource = tcs; + + // Reset counters and ensure results list exists + _testResults?.Clear(); + _testResults = _testResults ?? new List(); + _testCount = 0; + _passCount = 0; + _failCount = 0; + + // Extract parameters + string testMode = parameters["testMode"]?.ToObject()?.ToLower() ?? "editmode"; + string testFilter = parameters["testFilter"]?.ToObject(); + + // Validate test mode + if (!Enum.TryParse(testMode, true, out var mode)) { tcs.SetResult(McpUnitySocketHandler.CreateErrorResponse( - "Tests are already running. Please wait for them to complete.", - "test_runner_busy" + $"Invalid test mode '{testMode}'. Valid modes are: EditMode, PlayMode", + "validation_error" )); return; } - - // Extract parameters - string testModeStr = parameters["testMode"]?.ToObject() ?? "editmode"; - string testFilter = parameters["testFilter"]?.ToObject() ?? ""; - - // Parse test mode - TestMode testMode; - switch (testModeStr.ToLowerInvariant()) + + try { - case "playmode": - testMode = TestMode.PlayMode; - break; - case "editmode": - testMode = TestMode.EditMode; - break; - default: - testMode = TestMode.EditMode; - break; + _testRunnerService.ExecuteTests(mode, testFilter, tcs); + } + catch (Exception ex) + { + tcs.SetResult(McpUnitySocketHandler.CreateErrorResponse( + $"Failed to run tests: {ex.Message}", + "execution_error" + )); } - - // Log the execution - Debug.Log($"[MCP Unity] Running tests: Mode={testMode}, Filter={testFilter}"); - - // Reset state - _isRunning = true; - _testResults.Clear(); - _testRunCompletionSource = tcs; - - // Execute tests using the TestRunnerService - _testRunnerService.ExecuteTests( - testMode, - testFilter, - tcs - ); } - - #region ICallbacks Implementation - - // Called when a test run starts + public void RunStarted(ITestAdaptor testsToRun) { - Debug.Log($"[MCP Unity] Test run started: {testsToRun.Name}"); - } - - // Called when a test runs - public void TestStarted(ITestAdaptor test) - { - // Nothing to do here - } - - // Called when a test finishes - public void TestFinished(ITestResultAdaptor result) - { - _testResults.Add(new TestResult - { - Name = result.Test.Name, - FullName = result.FullName, - ResultState = result.ResultState, - Message = result.Message, - Duration = result.Duration - }); + if (testsToRun == null) return; - Debug.Log($"[MCP Unity] Test finished: {result.Test.Name} - {result.ResultState}"); + _testCount = testsToRun.TestCaseCount; + Debug.Log($"[MCP Unity] Starting test run with {_testCount} tests..."); } - - // Called when a test run completes - public void RunFinished(ITestResultAdaptor result) + + public void RunFinished(ITestResultAdaptor testResults) { - Debug.Log($"[MCP Unity] Test run completed: {result.Test.Name} - {result.ResultState}"); - - _isRunning = false; - - // Create test results summary - var summary = new JObject + try { - ["testCount"] = _testResults.Count, - ["passCount"] = _testResults.FindAll(r => r.Passed).Count, - ["duration"] = result.Duration, - ["success"] = result.ResultState == "Passed", - ["status"] = "completed", - ["message"] = $"Test run completed: {result.Test.Name} - {result.ResultState}" - }; - - // Add test results array - var resultArray = new JArray(); - foreach (var testResult in _testResults) + Debug.Log($"[MCP Unity] Tests completed: {_passCount} passed, {_failCount} failed"); + + var resultsArray = _testResults != null ? + JArray.FromObject(_testResults) : + new JArray(); + + var response = new JObject + { + ["success"] = _failCount == 0, + ["message"] = $"Tests completed: {_passCount} passed, {_failCount} failed", + ["testCount"] = _testCount, + ["passCount"] = _passCount, + ["failCount"] = _failCount, + ["results"] = resultsArray + }; + + _testCompletionSource?.TrySetResult(response); + } + catch (Exception ex) { - resultArray.Add(new JObject + Debug.LogError($"[MCP Unity] Error in RunFinished: {ex.Message}"); + if (_testCompletionSource != null && !_testCompletionSource.Task.IsCompleted) { - ["name"] = testResult.Name, - ["fullName"] = testResult.FullName, - ["result"] = testResult.ResultState, - ["message"] = testResult.Message, - ["duration"] = testResult.Duration - }); + _testCompletionSource.TrySetResult(McpUnitySocketHandler.CreateErrorResponse( + $"Error processing test results: {ex.Message}", + "result_error" + )); + } } - summary["results"] = resultArray; - - // Set the test run completion result + } + + public void TestStarted(ITestAdaptor test) + { + if (test?.Name == null) return; + Debug.Log($"[MCP Unity] Starting test: {test.Name}"); + } + + public void TestFinished(ITestResultAdaptor result) + { + if (result?.Test == null) return; + try { - _testRunCompletionSource.SetResult(new JObject + string status = result.TestStatus.ToString(); + Debug.Log($"[MCP Unity] Test finished: {result.Test.Name} - {status}"); + + if (result.TestStatus == TestStatus.Passed) + { + _passCount++; + } + else if (result.TestStatus == TestStatus.Failed) { - ["success"] = true, - ["type"] = "text", - ["message"] = summary["message"].Value() - }); + _failCount++; + } + + if (_testResults != null) + { + _testResults.Add(new JObject + { + ["name"] = result.Test?.Name ?? "Unknown Test", + ["status"] = status, + ["message"] = result.Message ?? string.Empty, + ["duration"] = result.Duration + }); + } } catch (Exception ex) { - Debug.LogError($"[MCP Unity] Failed to set test results: {ex.Message}"); - _testRunCompletionSource.TrySetException(ex); - } - finally - { - _testRunCompletionSource = null; + Debug.LogError($"[MCP Unity] Error in TestFinished: {ex.Message}"); } } - - #endregion } } diff --git a/Editor/Tools/SelectGameObjectTool.cs b/Editor/Tools/SelectGameObjectTool.cs index 8c7f686..1bc12bf 100644 --- a/Editor/Tools/SelectGameObjectTool.cs +++ b/Editor/Tools/SelectGameObjectTool.cs @@ -37,32 +37,46 @@ public override JObject Execute(JObject parameters) ); } + GameObject foundObject = null; + string identifier = ""; + // First try to find by instance ID if provided if (instanceId.HasValue) { - Selection.activeGameObject = EditorUtility.InstanceIDToObject(instanceId.Value) as GameObject; + foundObject = EditorUtility.InstanceIDToObject(instanceId.Value) as GameObject; + identifier = $"instance ID {instanceId.Value}"; } // Otherwise, try to find by object path/name if provided else { // Try to find the object by path in the hierarchy - Selection.activeGameObject = GameObject.Find(objectPath); + foundObject = GameObject.Find(objectPath); + identifier = $"path '{objectPath}'"; + } + + // Check if we actually found the object + if (foundObject == null) + { + return McpUnitySocketHandler.CreateErrorResponse( + $"GameObject with {identifier} not found", + "not_found_error" + ); } - // Ping the selected object - EditorGUIUtility.PingObject(Selection.activeGameObject); + // Set the selection and ping the object + Selection.activeGameObject = foundObject; + EditorGUIUtility.PingObject(foundObject); // Log the selection - Debug.Log($"[MCP Unity] Selected GameObject: " + - (instanceId.HasValue ? $"Instance ID {instanceId.Value}" : $"Path '{objectPath}'")); + Debug.Log($"[MCP Unity] Selected GameObject: {foundObject.name} (found by {identifier})"); - // Create the response + // Create the response with instanceId for tracking return new JObject { ["success"] = true, + ["message"] = $"Successfully selected GameObject: {foundObject.name}", ["type"] = "text", - ["message"] = $"Successfully selected GameObject" + - (instanceId.HasValue ? $" with instance ID: {instanceId.Value}" : $": {objectPath}") + ["instanceId"] = foundObject.GetInstanceID() }; } } diff --git a/Editor/Tools/UpdateComponentTool.cs b/Editor/Tools/UpdateComponentTool.cs index de88fd4..b8c6759 100644 --- a/Editor/Tools/UpdateComponentTool.cs +++ b/Editor/Tools/UpdateComponentTool.cs @@ -6,11 +6,12 @@ using UnityEngine; using UnityEditor; using Newtonsoft.Json.Linq; +using System.Linq; namespace McpUnity.Tools { /// - /// Tool for updating component data in the Unity Editor + /// Tool for updating component fields on GameObjects /// public class UpdateComponentTool : McpToolBase { @@ -26,104 +27,110 @@ public UpdateComponentTool() /// Tool parameters as a JObject public override JObject Execute(JObject parameters) { - // Extract parameters - int? instanceId = parameters["instanceId"]?.ToObject(); - string objectPath = parameters["objectPath"]?.ToObject(); - string componentName = parameters["componentName"]?.ToObject(); - JObject componentData = parameters["componentData"] as JObject; - - // Validate parameters - require either instanceId or objectPath - if (!instanceId.HasValue && string.IsNullOrEmpty(objectPath)) + GameObject targetObject = null; + + // Try to find by instance ID first + if (parameters["instanceId"] != null) + { + int instanceId = parameters["instanceId"].ToObject(); + targetObject = EditorUtility.InstanceIDToObject(instanceId) as GameObject; + } + // Then try by path + else if (parameters["objectPath"] != null) + { + string objectPath = parameters["objectPath"].ToObject(); + targetObject = GameObject.Find(objectPath); + } + + // Validate GameObject + if (targetObject == null) { return McpUnitySocketHandler.CreateErrorResponse( - "Either 'instanceId' or 'objectPath' must be provided", - "validation_error" + $"GameObject with path '{parameters["objectPath"]}' or instance ID {parameters["instanceId"]} not found", + "object_not_found" ); } - + + // Get component name and validate + string componentName = parameters["componentName"]?.ToObject(); if (string.IsNullOrEmpty(componentName)) { return McpUnitySocketHandler.CreateErrorResponse( - "Required parameter 'componentName' not provided", + "Required parameter 'componentName' not provided", "validation_error" ); } - - // Find the GameObject by instance ID or path - GameObject gameObject = null; - string identifier = "unknown"; - - if (instanceId.HasValue) - { - gameObject = EditorUtility.InstanceIDToObject(instanceId.Value) as GameObject; - identifier = $"ID {instanceId.Value}"; - } - else + + // Find component type + Type componentType = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => a.GetTypes()) + .FirstOrDefault(t => t.Name == componentName && typeof(Component).IsAssignableFrom(t)); + + if (componentType == null) { - // Find by path - gameObject = GameObject.Find(objectPath); - identifier = $"path '{objectPath}'"; - - if (gameObject == null) - { - // Try to find using the Unity Scene hierarchy path - gameObject = FindGameObjectByPath(objectPath); - } + return McpUnitySocketHandler.CreateErrorResponse( + $"Component type '{componentName}' not found in Unity", + "component_not_found" + ); } - - if (gameObject == null) + + // Get or add component + Component component = targetObject.GetComponent(componentType) ?? targetObject.AddComponent(componentType); + + // Get component data + JObject componentData = parameters["componentData"]?.ToObject(); + if (componentData == null) { return McpUnitySocketHandler.CreateErrorResponse( - $"GameObject with path '{objectPath}' or instance ID {instanceId} not found", - "not_found_error" + "Required parameter 'componentData' not provided", + "validation_error" ); } + + // Update component fields + List updateErrors = new List(); + if (componentData != null && componentData.Count > 0) + { + updateErrors = UpdateComponentData(component, componentData); + } - Debug.Log($"[MCP Unity] Updating component '{componentName}' on GameObject '{gameObject.name}' (found by {identifier})"); - - // Try to find the component by name - Component component = gameObject.GetComponent(componentName); - bool wasAdded = false; - - // If component not found, try to add it - if (component == null) + // Ensure changes are saved if there were no errors + if (updateErrors.Count == 0) { - Type componentType = FindComponentType(componentName); - if (componentType == null) + EditorUtility.SetDirty(targetObject); + if (PrefabUtility.IsPartOfAnyPrefab(targetObject)) { - return McpUnitySocketHandler.CreateErrorResponse( - $"Component type '{componentName}' not found in Unity", - "component_error" - ); + PrefabUtility.RecordPrefabInstancePropertyModifications(component); } - - component = Undo.AddComponent(gameObject, componentType); - wasAdded = true; - Debug.Log($"[MCP Unity] Added component '{componentName}' to GameObject '{gameObject.name}'"); } - // Update component fields - if (componentData != null && componentData.Count > 0) + // Create the response based on success/failure + bool success = updateErrors.Count == 0; + string message; + if (success) { - UpdateComponentData(component, componentData); + message = targetObject.GetComponent(componentType) == null + ? $"Successfully added component '{componentName}' to GameObject '{targetObject.name}' and updated its data" + : $"Successfully updated component '{componentName}' on GameObject '{targetObject.name}'"; } - - // Ensure changes are saved - EditorUtility.SetDirty(gameObject); - if (PrefabUtility.IsPartOfAnyPrefab(gameObject)) + else { - PrefabUtility.RecordPrefabInstancePropertyModifications(component); + message = $"Failed to fully update component '{componentName}' on GameObject '{targetObject.name}'. See errors for details."; } - - // Create the response - return new JObject + + JObject response = new JObject { - ["success"] = true, + ["success"] = success, ["type"] = "text", - ["message"] = wasAdded - ? $"Successfully added component '{componentName}' to GameObject '{gameObject.name}' and updated its data" - : $"Successfully updated component '{componentName}' on GameObject '{gameObject.name}'" + ["message"] = message }; + + if (!success) + { + response["errors"] = new JArray(updateErrors); + } + + return response; } /// @@ -235,16 +242,17 @@ private Type FindComponentType(string componentName) /// /// The component to update /// The data to apply to the component - /// True if the component was updated successfully - private bool UpdateComponentData(Component component, JObject componentData) + /// A list of JObjects detailing any errors encountered. Empty list means success. + private List UpdateComponentData(Component component, JObject componentData) { + List errors = new List(); if (component == null || componentData == null) { - return false; + errors.Add(new JObject { ["name"] = "component", ["reason"] = "Component or data was null" }); + return errors; } Type componentType = component.GetType(); - bool anySuccess = false; // Record object for undo Undo.RecordObject(component, $"Update {componentType.Name} fields"); @@ -252,33 +260,56 @@ private bool UpdateComponentData(Component component, JObject componentData) // Process each field in the component data foreach (var property in componentData.Properties()) { - string fieldName = property.Name; - JToken fieldValue = property.Value; + string memberName = property.Name; + JToken memberValue = property.Value; // Skip null values - if (fieldValue.Type == JTokenType.Null) + if (memberValue.Type == JTokenType.Null) { continue; } - - // Try to update field - FieldInfo fieldInfo = componentType.GetField(fieldName, - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - - if (fieldInfo != null) + + try { - object value = ConvertJTokenToValue(fieldValue, fieldInfo.FieldType); - fieldInfo.SetValue(component, value); - anySuccess = true; - continue; + PropertyInfo propInfo = componentType.GetProperty(memberName, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + + if (propInfo != null && propInfo.CanWrite) + { + object value = ConvertJTokenToValue(memberValue, propInfo.PropertyType); + propInfo.SetValue(component, value); + continue; // Successfully set property + } + + // If no writable property found, try fields + FieldInfo fieldInfo = componentType.GetField(memberName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + if (fieldInfo != null) + { + object value = ConvertJTokenToValue(memberValue, fieldInfo.FieldType); + fieldInfo.SetValue(component, value); + continue; // Successfully set field + } + + // If neither property nor field was found or usable + errors.Add(new JObject + { + ["name"] = memberName, + ["reason"] = propInfo != null ? "Property is read-only" : "Property or Field not found" + }); } - else + catch (Exception ex) // Catch errors during conversion or SetValue { - Debug.LogWarning($"[MCP Unity] Field '{fieldName}' not found on component '{componentType.Name}'"); + errors.Add(new JObject + { + ["name"] = memberName, + ["reason"] = $"Error setting value: {ex.Message}" // Include exception message + }); } } - return anySuccess; + return errors; } /// @@ -404,8 +435,9 @@ private object ConvertJTokenToValue(JToken token, Type targetType) } catch (Exception ex) { - Debug.LogError($"[MCP Unity] Error converting value to type {targetType.Name}: {ex.Message}"); - return null; + Debug.LogError($"[MCP Unity] Error converting value '{token}' to type {targetType.Name}: {ex.Message}"); + // Throw exception instead of returning null + throw new InvalidCastException($"Could not convert value '{token}' to type {targetType.Name}", ex); } } } diff --git a/Editor/UnityBridge/McpUnityServer.cs b/Editor/UnityBridge/McpUnityServer.cs index 18bfbce..a652bcd 100644 --- a/Editor/UnityBridge/McpUnityServer.cs +++ b/Editor/UnityBridge/McpUnityServer.cs @@ -66,6 +66,11 @@ public static McpUnityServer Instance /// public Dictionary Clients => _clients; + /// + /// Dictionary of registered resources + /// + public Dictionary Resources => _resources; + /// /// Private constructor to enforce singleton pattern /// diff --git a/Editor/UnityBridge/McpUnitySocketHandler.cs b/Editor/UnityBridge/McpUnitySocketHandler.cs index 774d58c..d51a1a4 100644 --- a/Editor/UnityBridge/McpUnitySocketHandler.cs +++ b/Editor/UnityBridge/McpUnitySocketHandler.cs @@ -77,7 +77,16 @@ protected override async void OnMessage(MessageEventArgs e) } else { - tcs.SetResult(CreateErrorResponse($"Unknown method: {method}", "unknown_method")); + // Check if we're trying to access a resource by URI + var resourceByUri = FindResourceByUri(method); + if (resourceByUri != null) + { + EditorCoroutineUtility.StartCoroutineOwnerless(FetchResourceCoroutine(resourceByUri, parameters, tcs)); + } + else + { + tcs.SetResult(CreateErrorResponse($"Unknown method: {method}", "unknown_method")); + } } // Wait for the task to complete @@ -90,7 +99,15 @@ protected override async void OnMessage(MessageEventArgs e) Debug.Log($"[MCP Unity] WebSocket message response: {responseStr}"); // Send the response back to the client - Send(responseStr); + try + { + Send(responseStr); + } + catch (Exception sendEx) + { + Debug.LogError($"[MCP Unity] Failed to send successful response to client (Request ID: {requestId}): {sendEx.Message}\nResponse was: {responseStr}"); + // Optionally: Rethrow or handle differently if needed, but avoid sending another message if connection is likely broken. + } } catch (Exception ex) { @@ -191,6 +208,29 @@ private IEnumerator FetchResourceCoroutine(McpResourceBase resource, JObject par yield return null; } + /// + /// Find a resource by its URI + /// + /// The URI to search for + /// The resource if found, null otherwise + private McpResourceBase FindResourceByUri(string uri) + { + // Get resources from the server + var resources = _server.Resources; + + // Look for a resource with a matching URI + foreach (var resource in resources.Values) + { + if (resource.Uri == uri) + { + Debug.Log($"[MCP Unity] Found resource {resource.Name} by URI {uri}"); + return resource; + } + } + + return null; + } + /// /// Create a JSON-RPC 2.0 response /// diff --git a/README.md b/README.md index 0a00ac8..6f2d6f0 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# MCP Unity Editor (Game Engine) +# MCP Unity Server -[![](https://badge.mcpx.dev?status=on 'MCP Enabled')](https://modelcontextprotocol.io/introduction) -[![](https://img.shields.io/badge/Unity-000000?style=flat&logo=unity&logoColor=white 'Unity')](https://unity.com/releases/editor/archive) -[![](https://img.shields.io/badge/Node.js-339933?style=flat&logo=nodedotjs&logoColor=white 'Node.js')](https://nodejs.org/en/download/) +The purpose of this package is to provide a MCP Unity Server for executing Unity operations and request Editor information from AI MCP enabled hosts. +## Setup + +### Requirements +- Unity 2022.3 or higher [![smithery badge](https://smithery.ai/badge/@CoderGamester/mcp-unity)](https://smithery.ai/server/@CoderGamester/mcp-unity) [![](https://img.shields.io/github/stars/CoderGamester/mcp-unity 'Stars')](https://github.com/CoderGamester/mcp-unity/stargazers) [![](https://img.shields.io/github/last-commit/CoderGamester/mcp-unity 'Last Commit')](https://github.com/CoderGamester/mcp-unity/commits/main) @@ -43,320 +45,35 @@ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ``` -MCP Unity is an implementation of the Model Context Protocol for Unity Editor, allowing AI assistants to interact with your Unity projects. This package provides a bridge between Unity and a Node.js server that implements the MCP protocol, enabling AI agents like Claude, Windsurf, and Cursor to execute operations within the Unity Editor. - -## Features - - - Unity MCP server - - -### IDE Integration - Package Cache Access - -MCP Unity provides automatic integration with VSCode-like IDEs (Visual Studio Code, Cursor, Windsurf) by adding the Unity `Library/PackedCache` folder to your workspace. This feature: - -- Improves code intelligence for Unity packages -- Enables better autocompletion and type information for Unity packages -- Helps AI coding assistants understand your project's dependencies - -### MCP Server Tools - -- `execute_menu_item`: Executes Unity menu items (functions tagged with the MenuItem attribute) - > **Example prompt:** "Execute the menu item 'GameObject/Create Empty' to create a new empty GameObject" - -- `select_gameobject`: Selects game objects in the Unity hierarchy by path or instance ID - > **Example prompt:** "Select the Main Camera object in my scene" - -- `update_component`: Updates component fields on a GameObject or adds it to the GameObject if it does not contain the component - > **Example prompt:** "Add a Rigidbody component to the Player object and set its mass to 5" - -- `add_package`: Installs new packages in the Unity Package Manager - > **Example prompt:** "Add the TextMeshPro package to my project" - -- `run_tests`: Runs tests using the Unity Test Runner - > **Example prompt:** "Run all the EditMode tests in my project" - -- `notify_message`: Displays messages in the Unity Editor - > **Example prompt:** "Send a notification to Unity that the task has been completed" - -- `add_asset_to_scene`: Adds an asset from the AssetDatabase to the Unity scene - > **Example prompt:** "Add the Player prefab from my project to the current scene" - -### MCP Server Resources - -- `unity://menu-items`: Retrieves a list of all available menu items in the Unity Editor to facilitate `execute_menu_item` tool - > **Example prompt:** "Show me all available menu items related to GameObject creation" - -- `unity://hierarchy`: Retrieves a list of all game objects in the Unity hierarchy - > **Example prompt:** "Show me the current scene hierarchy structure" - -- `unity://gameobject/{id}`: Retrieves detailed information about a specific GameObject by instance ID or object path in the scene hierarchy, including all GameObject components with it's serialized properties and fields - > **Example prompt:** "Get me detailed information about the Player GameObject" - -- `unity://logs`: Retrieves a list of all logs from the Unity console - > **Example prompt:** "Show me the recent error messages from the Unity console" - -- `unity://packages`: Retrieves information about installed and available packages from the Unity Package Manager - > **Example prompt:** "List all the packages currently installed in my Unity project" - -- `unity://assets`: Retrieves information about assets in the Unity Asset Database - > **Example prompt:** "Find all texture assets in my project" - -- `unity://tests/{testMode}`: Retrieves information about tests in the Unity Test Runner - > **Example prompt:** "List all available tests in my Unity project" - -## Requirements -- Unity 2022.3 or later - to [install the server](#install-server) -- Node.js 18 or later - to [start the server](#start-server) -- npm 9 or later - to [debug the server](#debug-server) - -## Installation - -Installing this MCP Unity Server is a multi-step process: - -### Step 1: Install Unity MCP Server package via Unity Package Manager -1. Open the Unity Package Manager (Window > Package Manager) -2. Click the "+" button in the top-left corner -3. Select "Add package from git URL..." -4. Enter: `https://github.com/CoderGamester/mcp-unity.git` -5. Click "Add" - -![package manager](https://github.com/user-attachments/assets/a72bfca4-ae52-48e7-a876-e99c701b0497) - - -### Step 2: Install Node.js -> To run MCP Unity server, you'll need to have Node.js 18 or later installed on your computer: - -
-Windows - -1. Visit the [Node.js download page](https://nodejs.org/en/download/) -2. Download the Windows Installer (.msi) for the LTS version (recommended) -3. Run the installer and follow the installation wizard -4. Verify the installation by opening PowerShell and running: - ```bash - node --version - ``` -
- -
-macOS - -1. Visit the [Node.js download page](https://nodejs.org/en/download/) -2. Download the macOS Installer (.pkg) for the LTS version (recommended) -3. Run the installer and follow the installation wizard -4. Alternatively, if you have Homebrew installed, you can run: - ```bash - brew install node@18 - ``` -5. Verify the installation by opening Terminal and running: - ```bash - node --version - ``` -
- -### Step 3: Configure AI LLM Client - -
-Option 1: Configure using Unity Editor - -1. Open the Unity Editor -2. Navigate to Tools > MCP Unity > Server Window -3. Click on the "Configure" button for your AI LLM client as shown in the image below - -![image](https://github.com/user-attachments/assets/8d286e83-da60-40fa-bd6c-5de9a77c1820) - -4. Confirm the configuration installation with the given popup - -![image](https://github.com/user-attachments/assets/b1f05d33-3694-4256-a57b-8556005021ba) - -
- -
-Option 2: Configure via Smithery +### Installation +1. Open the Package Manager in Unity +2. Click the "+" button and select "Add package from git URL..." +3. Enter the repository URL: `https://github.com/CoderGamester/mcp-unity.git` +4. Click "Add" -To install MCP Unity via [Smithery](https://smithery.ai/server/@CoderGamester/mcp-unity): - -``` -Currently not available +Or add the following dependency to your `manifest.json`: +```json +"com.gamelovers.mcp-unity": "https://github.com/CoderGamester/mcp-unity.git" ``` -
- -
-Option 3: Configure Manually - -Open the MCP configuration file of your AI client (e.g. claude_desktop_config.json in Claude Desktop) and copy the following text: - -> Replace `ABSOLUTE/PATH/TO` with the absolute path to your MCP Unity installation or just copy the text from the Unity Editor MCP Server window (Tools > MCP Unity > Server Window). +For local development, you can add the package via the filesystem: ```json -{ - "mcpServers": { - "mcp-unity": { - "command": "node", - "args": [ - "ABSOLUTE/PATH/TO/mcp-unity/Server/build/index.js" - ], - "env": { - "UNITY_PORT": "8090" - } - } - } -} +"com.gamelovers.mcp-unity": "file:/path/to/mcp-unity" ``` -
- -## Start Unity Editor MCP Server -1. Open the Unity Editor -2. Navigate to Tools > MCP Unity > Server Window -3. Click "Start Server" to start the WebSocket server -4. Open Claude Desktop or your AI Coding IDE (e.g. Cursor IDE, Windsurf IDE, etc.) and start executing Unity tools - -![connect](https://github.com/user-attachments/assets/2e266a8b-8ba3-4902-b585-b220b11ab9a2) - -> When the AI client connects to the WebSocket server, it will automatically show in the green box in the window - -## Optional: Set WebSocket Port -By default, the WebSocket server runs on port 8090. You can change this port in two ways: - -
-Option 1: Using the Unity Editor - -1. Open the Unity Editor -2. Navigate to Tools > MCP Unity > Server Window -3. Change the "WebSocket Port" value to your desired port number -4. Unity will setup the system environment variable UNITY_PORT to the new port number -5. Restart the Node.js server -6. Click again on "Start Server" to reconnect the Unity Editor web socket to the Node.js MCP Server - -
- -
-Option 2: Using the terminal - -1. Set the UNITY_PORT environment variable in the terminal - - Powershell - ```powershell - $env:UNITY_PORT = "8090" - ``` - - Command Prompt/Terminal - ```cmd - set UNITY_PORT=8090 - ``` -2. Restart the Node.js server -3. Click again on "Start Server" to reconnect the Unity Editor web socket to the Node.js MCP Server - -
- -## Debugging the Server - -
-Building the Node.js Server - -The MCP Unity server is built using Node.js . It requires to compile the TypeScript code to JavaScript in the `build` directory. -To build the server, open a terminal and: - -1. Navigate to the Server directory: - ```bash - cd ABSOLUTE/PATH/TO/mcp-unity/Server - ``` - -2. Install dependencies: - ```bash - npm install - ``` - -3. Build the server: - ```bash - npm run build - ``` +## Running Tests -4. Run the server: - ```bash - node build/index.js - ``` +Due to how Unity handles tests in local packages, you may need to manually copy test files into your project to run them: -
- -
-Debugging with MCP Inspector +1. Create a new folder at `Assets/McpTests` +2. Copy the test files from `Packages/com.gamelovers.mcp-unity/Tests/Editor` to `Assets/McpTests` +3. Open the Test Runner window (Window > General > Test Runner) +4. Tests should now appear in the EditMode tab -Debug the server with [@modelcontextprotocol/inspector](https://github.com/modelcontextprotocol/inspector): - - Powershell - ```powershell - $env:UNITY_PORT=8090; npx @modelcontextprotocol/inspector node Server/build/index.js - ``` - - Command Prompt/Terminal - ```cmd - set UNITY_PORT=8090 && npx @modelcontextprotocol/inspector node Server/build/index.js - ``` - -Don't forget to shutdown the server with `Ctrl + C` before closing the terminal or debugging it with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). - -
- -
-Enable Console Logs - -1. Enable logging on your terminal or into a log.txt file: - - Powershell - ```powershell - $env:LOGGING = "true" - $env:LOGGING_FILE = "true" - ``` - - Command Prompt/Terminal - ```cmd - set LOGGING=true - set LOGGING_FILE=true - ``` - -
- -## Troubleshooting - -
-Connection Issues - -- Ensure the WebSocket server is running (check the Server Window in Unity) -- Check if there are any firewall restrictions blocking the connection -- Make sure the port number is correct (default is 8080) -- Change the port number in the Unity Editor MCP Server window. (Tools > MCP Unity > Server Window) -
- -
-Server Not Starting - -- Check the Unity Console for error messages -- Ensure Node.js is properly installed and accessible in your PATH -- Verify that all dependencies are installed in the Server directory -
- -
-Menu Items Not Executing - -- Ensure the menu item path is correct (case-sensitive) -- Check if the menu item requires confirmation -- Verify that the menu item is available in the current context -
- -## Support & Feedback - -If you have any questions or need support, please open an [issue](https://github.com/CoderGamester/mcp-unity/issues) on this repository. - -Alternative you can reach out on: -- Linkedin: [![](https://img.shields.io/badge/LinkedIn-0077B5?style=flat&logo=linkedin&logoColor=white 'LinkedIn')](https://www.linkedin.com/in/miguel-tomas/) -- Discord: gamester7178 -- Email: game.gamester@gmail.com - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request or open an Issue with your request. - -**Commit your changes** following the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format. - -## License +## Further Documentation +See the [Wiki](https://github.com/CoderGamester/mcp-unity/wiki) for full documentation. +======= This project is under [MIT License](License.md) ## Acknowledgements @@ -365,3 +82,4 @@ This project is under [MIT License](License.md) - [Unity Technologies](https://unity.com) - [Node.js](https://nodejs.org) - [WebSocket-Sharp](https://github.com/sta/websocket-sharp) + diff --git a/Server/build/tools/selectGameObjectTool.js b/Server/build/tools/selectGameObjectTool.js index 73094df..13ed70a 100644 --- a/Server/build/tools/selectGameObjectTool.js +++ b/Server/build/tools/selectGameObjectTool.js @@ -30,6 +30,33 @@ export function createSelectGameObjectTool(server, mcpUnity, logger) { throw error; } }); + return { + name: toolName, + description: toolDescription, + paramsSchema: paramsSchema, + handler: async (params) => { + // Custom validation since we can't use refine/superRefine while maintaining ZodObject type + if (params.objectPath === undefined && params.instanceId === undefined) { + throw new McpUnityError(ErrorType.VALIDATION, "Either 'objectPath' or 'instanceId' must be provided"); + } + const response = await mcpUnity.sendRequest({ + method: toolName, + params + }); + if (!response.success) { + throw new McpUnityError(ErrorType.TOOL_EXECUTION, response.message || `Failed to select GameObject`); + } + return { + success: true, + message: response.message, + content: [{ + type: response.type, + text: response.message || `Successfully selected GameObject`, + instanceId: response.instanceId + }] + }; + } + }; } /** * Handles selecting a GameObject in Unity @@ -40,10 +67,6 @@ export function createSelectGameObjectTool(server, mcpUnity, logger) { * @throws McpUnityError if the request to Unity fails */ async function toolHandler(mcpUnity, params) { - // Custom validation since we can't use refine/superRefine while maintaining ZodObject type - if (params.objectPath === undefined && params.instanceId === undefined) { - throw new McpUnityError(ErrorType.VALIDATION, "Either 'objectPath' or 'instanceId' must be provided"); - } const response = await mcpUnity.sendRequest({ method: toolName, params diff --git a/Server/build/unity/mcpUnity.js b/Server/build/unity/mcpUnity.js index d19b83f..07adf81 100644 --- a/Server/build/unity/mcpUnity.js +++ b/Server/build/unity/mcpUnity.js @@ -90,18 +90,33 @@ export class McpUnity { */ handleMessage(data) { try { + this.logger.debug(`Received raw message from Unity: ${data}`); const response = JSON.parse(data); + this.logger.debug(`Parsed response ID: ${response.id}`); if (response.id && this.pendingRequests.has(response.id)) { + this.logger.debug(`Found pending request for ID: ${response.id}`); const request = this.pendingRequests.get(response.id); clearTimeout(request.timeout); this.pendingRequests.delete(response.id); if (response.error) { + this.logger.warn(`Received error from Unity for ID ${response.id}: ${response.error.message}`); request.reject(new McpUnityError(ErrorType.TOOL_EXECUTION, response.error.message || 'Unknown error', response.error.details)); } else { + this.logger.info(`Received success from Unity for ID ${response.id}. Resolving promise.`); + try { + this.logger.debug(`Resolving with result: ${JSON.stringify(response.result)}`); + } + catch (stringifyError) { + this.logger.warn(`Could not stringify result for logging: ${stringifyError}`); + this.logger.debug(`Resolving with result (object):`, response.result); + } request.resolve(response.result); } } + else { + this.logger.warn(`Received message from Unity with unknown or missing ID: ${response.id}`); + } } catch (e) { this.logger.error(`Error parsing WebSocket message: ${e instanceof Error ? e.message : String(e)}`); diff --git a/Server/src/tools/selectGameObjectTool.ts b/Server/src/tools/selectGameObjectTool.ts index 64cb6a5..942e057 100644 --- a/Server/src/tools/selectGameObjectTool.ts +++ b/Server/src/tools/selectGameObjectTool.ts @@ -4,6 +4,7 @@ import { McpUnity } from '../unity/mcpUnity.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { McpUnityError, ErrorType } from '../utils/errors.js'; import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { ToolDefinition } from './toolRegistry.js'; // Constants for the tool const toolName = 'select_gameobject'; @@ -21,7 +22,7 @@ const paramsSchema = z.object({ * @param mcpUnity The McpUnity instance to communicate with Unity * @param logger The logger instance for diagnostic information */ -export function createSelectGameObjectTool(server: McpServer, mcpUnity: McpUnity, logger: Logger) { +export function createSelectGameObjectTool(server: McpServer, mcpUnity: McpUnity, logger: Logger): ToolDefinition { logger.info(`Registering tool: ${toolName}`); // Register this tool with the MCP server @@ -41,6 +42,43 @@ export function createSelectGameObjectTool(server: McpServer, mcpUnity: McpUnity } } ); + + return { + name: toolName, + description: toolDescription, + paramsSchema: paramsSchema, + handler: async (params): Promise => { + // Custom validation since we can't use refine/superRefine while maintaining ZodObject type + if (params.objectPath === undefined && params.instanceId === undefined) { + throw new McpUnityError( + ErrorType.VALIDATION, + "Either 'objectPath' or 'instanceId' must be provided" + ); + } + + const response = await mcpUnity.sendRequest({ + method: toolName, + params + }); + + if (!response.success) { + throw new McpUnityError( + ErrorType.TOOL_EXECUTION, + response.message || `Failed to select GameObject` + ); + } + + return { + success: true, + message: response.message, + content: [{ + type: response.type, + text: response.message || `Successfully selected GameObject`, + instanceId: response.instanceId + }] + }; + } + }; } /** @@ -52,14 +90,6 @@ export function createSelectGameObjectTool(server: McpServer, mcpUnity: McpUnity * @throws McpUnityError if the request to Unity fails */ async function toolHandler(mcpUnity: McpUnity, params: any): Promise { - // Custom validation since we can't use refine/superRefine while maintaining ZodObject type - if (params.objectPath === undefined && params.instanceId === undefined) { - throw new McpUnityError( - ErrorType.VALIDATION, - "Either 'objectPath' or 'instanceId' must be provided" - ); - } - const response = await mcpUnity.sendRequest({ method: toolName, params diff --git a/Server/src/unity/mcpUnity.ts b/Server/src/unity/mcpUnity.ts index b08e5eb..56a5099 100644 --- a/Server/src/unity/mcpUnity.ts +++ b/Server/src/unity/mcpUnity.ts @@ -132,22 +132,35 @@ export class McpUnity { */ private handleMessage(data: string): void { try { + this.logger.debug(`Received raw message from Unity: ${data}`); const response = JSON.parse(data) as UnityResponse; + this.logger.debug(`Parsed response ID: ${response.id}`); if (response.id && this.pendingRequests.has(response.id)) { + this.logger.debug(`Found pending request for ID: ${response.id}`); const request = this.pendingRequests.get(response.id)!; clearTimeout(request.timeout); this.pendingRequests.delete(response.id); if (response.error) { + this.logger.warn(`Received error from Unity for ID ${response.id}: ${response.error.message}`); request.reject(new McpUnityError( ErrorType.TOOL_EXECUTION, response.error.message || 'Unknown error', response.error.details )); } else { + this.logger.info(`Received success from Unity for ID ${response.id}. Resolving promise.`); + try { + this.logger.debug(`Resolving with result: ${JSON.stringify(response.result)}`); + } catch (stringifyError) { + this.logger.warn(`Could not stringify result for logging: ${stringifyError}`); + this.logger.debug(`Resolving with result (object):`, response.result); + } request.resolve(response.result); } + } else { + this.logger.warn(`Received message from Unity with unknown or missing ID: ${response.id}`); } } catch (e) { this.logger.error(`Error parsing WebSocket message: ${e instanceof Error ? e.message : String(e)}`); diff --git a/Tests.meta b/Tests.meta new file mode 100644 index 0000000..ecf9274 --- /dev/null +++ b/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 533889eb8189e4918a2a97652e8e5561 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/.gitignore b/Tests/.gitignore new file mode 100644 index 0000000..268cb3a --- /dev/null +++ b/Tests/.gitignore @@ -0,0 +1,2 @@ +# Exclude meta files +*.meta \ No newline at end of file diff --git a/Tests/Editor/AddAssetToSceneToolTests.cs b/Tests/Editor/AddAssetToSceneToolTests.cs new file mode 100644 index 0000000..ff20fc9 --- /dev/null +++ b/Tests/Editor/AddAssetToSceneToolTests.cs @@ -0,0 +1,27 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using UnityEditor; +using McpUnity.Tools; +using Newtonsoft.Json.Linq; + +namespace McpUnity.Tests +{ + public class AddAssetToSceneToolTests + { + [Test] + public void SimpleTest() + { + Debug.Log("[MCP Unity Test] Running simple verification test"); + Assert.Pass("Simple test passed"); + } + + [Test] + public void AnotherSimpleTest() + { + Debug.Log("[MCP Unity Test] Running another verification test"); + Assert.That(true, Is.True, "Truth value should be true"); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/AddAssetToSceneToolTests.cs.meta b/Tests/Editor/AddAssetToSceneToolTests.cs.meta new file mode 100644 index 0000000..35a7c14 --- /dev/null +++ b/Tests/Editor/AddAssetToSceneToolTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1e34e0c0674a84ec78185317ad7ca289 \ No newline at end of file diff --git a/Tests/Editor/BasicTests.cs b/Tests/Editor/BasicTests.cs new file mode 100644 index 0000000..f552b94 --- /dev/null +++ b/Tests/Editor/BasicTests.cs @@ -0,0 +1,15 @@ +using NUnit.Framework; +using UnityEngine; + +namespace McpUnity.Tests +{ + public class BasicTests + { + [Test] + public void BasicTest() + { + Debug.Log("[MCP Unity Test] Running basic test"); + Assert.That(1 + 1, Is.EqualTo(2), "Basic math should work"); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/BasicTests.cs.meta b/Tests/Editor/BasicTests.cs.meta new file mode 100644 index 0000000..bf8a9b0 --- /dev/null +++ b/Tests/Editor/BasicTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e9a910c5571b5473f944f68a35b5d398 \ No newline at end of file diff --git a/Tests/Editor/McpUnity.Tests.Editor.asmdef b/Tests/Editor/McpUnity.Tests.Editor.asmdef new file mode 100644 index 0000000..88f31f1 --- /dev/null +++ b/Tests/Editor/McpUnity.Tests.Editor.asmdef @@ -0,0 +1,25 @@ +{ + "name": "McpUnity.Editor.Tests", + "rootNamespace": "McpUnity.Tests", + "references": [ + "McpUnity.Editor" + ], + "optionalUnityReferences": [ + "TestAssemblies" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll", + "Newtonsoft.Json.dll" + ], + "autoReferenced": true, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Tests/Editor/SimpleTest.cs b/Tests/Editor/SimpleTest.cs new file mode 100644 index 0000000..36d5b18 --- /dev/null +++ b/Tests/Editor/SimpleTest.cs @@ -0,0 +1,15 @@ +using NUnit.Framework; +using UnityEngine; + +namespace McpUnity.Tests +{ + public class SimpleTest + { + [Test] + public void SimplePassingTest() + { + Debug.Log("Running SimplePassingTest"); + Assert.Pass("This test should pass"); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/SimpleTest.cs.meta b/Tests/Editor/SimpleTest.cs.meta new file mode 100644 index 0000000..8d79e94 --- /dev/null +++ b/Tests/Editor/SimpleTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b7da8eced07424bf1975d3683e8a405f \ No newline at end of file diff --git a/Tests/Editor/TestRunner.cs b/Tests/Editor/TestRunner.cs new file mode 100644 index 0000000..9ad4bf7 --- /dev/null +++ b/Tests/Editor/TestRunner.cs @@ -0,0 +1,24 @@ +using System; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace McpUnity.Tests +{ + public class TestRunner + { + [Test] + public void SimplePassTest() + { + Debug.Log("Running a very simple pass test"); + Assert.Pass("This test should always pass"); + } + + [Test] + public void SimpleAssertionTest() + { + Debug.Log("Running simple assertion test"); + Assert.That(1 + 1, Is.EqualTo(2), "Basic math should work"); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/TestRunner.cs.meta b/Tests/Editor/TestRunner.cs.meta new file mode 100644 index 0000000..f05256d --- /dev/null +++ b/Tests/Editor/TestRunner.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3d226f9563cb544d4b46ee7caaa75381 \ No newline at end of file diff --git a/Tests/McpTests.md b/Tests/McpTests.md new file mode 100644 index 0000000..4b0858a --- /dev/null +++ b/Tests/McpTests.md @@ -0,0 +1,41 @@ +# Instructions for Using MCP Unity Tests + +Since Unity has specific requirements for package test discovery, we recommend the following workaround if tests aren't appearing in the Test Runner: + +1. In your main project, create a new folder at `Assets/McpTests` +2. Copy the test files from `Packages/com.gamelovers.mcp-unity/Tests/Editor` to `Assets/McpTests` +3. Create a new Assembly Definition file in `Assets/McpTests` with the following configuration: + +```json +{ + "name": "McpUnity.Tests.Editor", + "rootNamespace": "McpUnity.Tests", + "references": [ + "GUID:27619889b8ba8c24980f49ee34dbb44a", + "GUID:0acc523941302664db1f4e527237feb3", + "McpUnity.Editor" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll", + "Newtonsoft.Json.dll" + ], + "autoReferenced": true, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} +``` + +4. Save the assembly definition file and allow Unity to compile it +5. Open the Test Runner window (Window > General > Test Runner) +6. Tests should now appear in the EditMode tab + +This workaround is necessary because Unity sometimes has difficulty discovering tests in local packages. When the package is published to a registry, this workaround should not be needed. \ No newline at end of file diff --git a/Tests/README.md b/Tests/README.md new file mode 100644 index 0000000..67b4aa8 --- /dev/null +++ b/Tests/README.md @@ -0,0 +1,27 @@ +# MCP Unity Tests + +This directory contains unit tests for the MCP Unity Server package. + +## Important: Test Discovery in Unity + +Unity has specific requirements for package test discovery when using local packages. While these tests follow the standard package structure, they may not appear in the Test Runner when the package is included as a local package. + +## Workaround for Local Development + +To run these tests in your project: + +1. Create a new folder at `Assets/McpTests` in your main project +2. Copy all the test files from `Packages/com.gamelovers.mcp-unity/Tests/Editor` to `Assets/McpTests` +3. Open the Test Runner window (Window > General > Test Runner) +4. Tests should now appear in the EditMode tab + +This workaround is necessary because Unity sometimes has difficulty discovering tests in local packages. When the package is published to a registry, this workaround should not be needed. + +## Test Structure + +Tests are organized following Unity's package layout guidelines: + +- `Editor`: Contains tests that run in Edit mode +- `Runtime`: Contains tests that run in Play mode (currently not implemented) + +Each directory has its own assembly definition file to ensure proper reference handling. \ No newline at end of file diff --git a/Tests/Runtime/McpUnity.Tests.Runtime.asmdef b/Tests/Runtime/McpUnity.Tests.Runtime.asmdef new file mode 100644 index 0000000..3a284f9 --- /dev/null +++ b/Tests/Runtime/McpUnity.Tests.Runtime.asmdef @@ -0,0 +1,21 @@ +{ + "name": "McpUnity.Tests", + "rootNamespace": "McpUnity.Tests", + "references": [], + "optionalUnityReferences": [ + "TestAssemblies" + ], + "includePlatforms": [], + "excludePlatforms": [], + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll", + "Newtonsoft.Json.dll" + ], + "autoReferenced": true, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Tests/Runtime/SimplePlayModeTest.cs b/Tests/Runtime/SimplePlayModeTest.cs new file mode 100644 index 0000000..5e9282d --- /dev/null +++ b/Tests/Runtime/SimplePlayModeTest.cs @@ -0,0 +1,26 @@ +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace McpUnity.Tests +{ + public class SimplePlayModeTest + { + [Test] + public void SimplePlayModeTestPasses() + { + Debug.Log("Running Simple PlayMode Test"); + Assert.Pass("This play mode test should pass"); + } + + [UnityTest] + public IEnumerator SimplePlayModeUnityTestWithEnumeratorPasses() + { + Debug.Log("Running Simple PlayMode Unity Test"); + yield return null; + Assert.Pass("This play mode unity test should pass"); + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/SimplePlayModeTest.cs.meta b/Tests/Runtime/SimplePlayModeTest.cs.meta new file mode 100644 index 0000000..1e16a39 --- /dev/null +++ b/Tests/Runtime/SimplePlayModeTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7a7234a8cd0a64129a1a0f70363f71b0 \ No newline at end of file diff --git a/package.json b/package.json index 8a3efcc..55b6e52 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,22 @@ { "name": "com.gamelovers.mcp-unity", - "displayName": "MCP Unity Server", - "author": "CoderGamester", "version": "1.0.0", - "unity": "2022.3", - "license": "MIT", + "displayName": "MCP Unity Server", "description": "The purpose of this package is to provide a MCP Unity Server for executing Unity operations and request Editor information from AI MCP enabled hosts", + "unity": "2022.3", "dependencies": { "com.unity.nuget.newtonsoft-json": "3.2.1", - "com.unity.editorcoroutines": "1.0.0" + "com.unity.editorcoroutines": "1.0.0", + "com.unity.test-framework": "1.1.33" + }, + "keywords": [ + "mcp", + "unity" + ], + "author": { + "name": "CoderGamester" }, - "type": "library", - "hideInEditor": false + "testables": [ + "com.gamelovers.mcp-unity" + ] } \ No newline at end of file