Skip to content

Commit 738445e

Browse files
committed
feat: Enhance MCP tools and add test framework
Improves validation, error handling, and robustness of several MCP Unity tools. Simultaneously introduces a new testing framework structure under (migrated from ) to facilitate automated testing. **Key Tool Enhancements:** * **NotifyMessageTool**: Validates message content and type (info, warning, error enum). * **RunTestsTool**: Refactored execution, added mode validation, provides detailed pass/fail counts and individual results. Handles 'no tests found' case. * **SelectGameObjectTool**: Requires instanceId/objectPath, improves search logic (ID > path > name), specific 'not found' errors. * **UpdateComponentTool**: Requires componentName/Data, improves search (ID > path), finds type across assemblies, adds component if missing, uses Reflection for safer updates, reports invalid fields. * **McpUnitySocketHandler**: Added capability to look up Resources by URI. * **General**: More specific error codes/messages for various failures. **Test Framework Setup:** * Adds new testing structure under the top-level directory. * Includes Assembly Definitions () for EditMode and PlayMode tests. * Adds initial test files () and necessary files. * Moves/reorganizes existing test files from to the new structure. * Updates , , and accordingly.
1 parent 03d1ce9 commit 738445e

29 files changed

+517
-471
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
**/[Uu]ser[Ss]ettings/
1010
**/[Oo]bj/Debug/
1111

12+
# Development logs and scripts
13+
log.txt
14+
log.txt.meta
15+
**/run_mcp_server_with_logging.sh
16+
**/run_mcp_server_with_logging.sh.meta
17+
1218
# Asset meta data should only be ignored when the corresponding asset is also ignored
1319
!**/[Aa]ssets/**/*.meta
1420

Editor/Services/TestRunnerService.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public async void ExecuteTests(
5555
TestMode testMode,
5656
string testFilter,
5757
TaskCompletionSource<JObject> completionSource,
58-
int timeoutMinutes = 1)
58+
int timeoutMinutes = 10)
5959
{
6060
// Create filter
6161
var filter = new Filter

Editor/Tests.meta

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/Tools/AddAssetToSceneTool.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,12 @@ public override JObject Execute(JObject parameters)
113113
}
114114
else
115115
{
116-
Debug.LogWarning($"[MCP Unity] Parent object not found, asset will be created at the root of the scene");
116+
// Parent not found, return an error instead of adding to root
117+
string identifier = parentId.HasValue ? $"ID {parentId.Value}" : $"path '{parentPath}'";
118+
return McpUnitySocketHandler.CreateErrorResponse(
119+
$"Parent GameObject not found with {identifier}",
120+
"parent_not_found"
121+
);
117122
}
118123
}
119124

Editor/Tools/RunTestsTool.cs

+75-46
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ public class RunTestsTool : McpToolBase, ICallbacks
1717
{
1818
private readonly ITestRunnerService _testRunnerService;
1919
private TaskCompletionSource<JObject> _testCompletionSource;
20+
private List<JObject> _testResults;
2021
private int _testCount;
2122
private int _passCount;
2223
private int _failCount;
23-
private List<JObject> _testResults;
2424

2525
public RunTestsTool(ITestRunnerService testRunnerService)
2626
{
@@ -30,6 +30,7 @@ public RunTestsTool(ITestRunnerService testRunnerService)
3030

3131
_testRunnerService = testRunnerService;
3232
_testRunnerService.TestRunnerApi.RegisterCallbacks(this);
33+
_testResults = new List<JObject>();
3334
}
3435

3536
public override JObject Execute(JObject parameters)
@@ -39,8 +40,18 @@ public override JObject Execute(JObject parameters)
3940

4041
public override void ExecuteAsync(JObject parameters, TaskCompletionSource<JObject> tcs)
4142
{
43+
_testCompletionSource = tcs;
44+
45+
// Reset counters and ensure results list exists
46+
_testResults?.Clear();
47+
_testResults = _testResults ?? new List<JObject>();
48+
_testCount = 0;
49+
_passCount = 0;
50+
_failCount = 0;
51+
4252
// Extract parameters
43-
string testMode = parameters["testMode"]?.ToObject<string>() ?? "EditMode";
53+
string testMode = parameters["testMode"]?.ToObject<string>()?.ToLower() ?? "editmode";
54+
string testFilter = parameters["testFilter"]?.ToObject<string>();
4455

4556
// Validate test mode
4657
if (!Enum.TryParse<TestMode>(testMode, true, out var mode))
@@ -52,16 +63,8 @@ public override void ExecuteAsync(JObject parameters, TaskCompletionSource<JObje
5263
return;
5364
}
5465

55-
// Initialize test tracking
56-
_testCount = 0;
57-
_passCount = 0;
58-
_failCount = 0;
59-
_testResults = new List<JObject>();
60-
_testCompletionSource = tcs;
61-
6266
try
6367
{
64-
string testFilter = parameters["testFilter"]?.ToObject<string>();
6568
_testRunnerService.ExecuteTests(mode, testFilter, tcs);
6669
}
6770
catch (Exception ex)
@@ -75,60 +78,86 @@ public override void ExecuteAsync(JObject parameters, TaskCompletionSource<JObje
7578

7679
public void RunStarted(ITestAdaptor testsToRun)
7780
{
78-
_testCount = testsToRun.TestCaseCount;
81+
if (testsToRun == null) return;
7982

80-
if (_testCount == 0)
81-
{
82-
_testCompletionSource.TrySetResult(new JObject
83-
{
84-
["success"] = false,
85-
["message"] = "No tests found matching the specified criteria",
86-
["testCount"] = 0,
87-
["passCount"] = 0,
88-
["failCount"] = 0,
89-
["results"] = new JArray()
90-
});
91-
}
83+
_testCount = testsToRun.TestCaseCount;
84+
Debug.Log($"[MCP Unity] Starting test run with {_testCount} tests...");
9285
}
9386

9487
public void RunFinished(ITestResultAdaptor testResults)
9588
{
96-
var response = new JObject
89+
try
90+
{
91+
Debug.Log($"[MCP Unity] Tests completed: {_passCount} passed, {_failCount} failed");
92+
93+
var resultsArray = _testResults != null ?
94+
JArray.FromObject(_testResults) :
95+
new JArray();
96+
97+
var response = new JObject
98+
{
99+
["success"] = _failCount == 0,
100+
["message"] = $"Tests completed: {_passCount} passed, {_failCount} failed",
101+
["testCount"] = _testCount,
102+
["passCount"] = _passCount,
103+
["failCount"] = _failCount,
104+
["results"] = resultsArray
105+
};
106+
107+
_testCompletionSource?.TrySetResult(response);
108+
}
109+
catch (Exception ex)
97110
{
98-
["success"] = _failCount == 0,
99-
["message"] = $"Tests completed: {_passCount} passed, {_failCount} failed",
100-
["testCount"] = _testCount,
101-
["passCount"] = _passCount,
102-
["failCount"] = _failCount,
103-
["results"] = JArray.FromObject(_testResults)
104-
};
105-
106-
_testCompletionSource.TrySetResult(response);
111+
Debug.LogError($"[MCP Unity] Error in RunFinished: {ex.Message}");
112+
if (_testCompletionSource != null && !_testCompletionSource.Task.IsCompleted)
113+
{
114+
_testCompletionSource.TrySetResult(McpUnitySocketHandler.CreateErrorResponse(
115+
$"Error processing test results: {ex.Message}",
116+
"result_error"
117+
));
118+
}
119+
}
107120
}
108121

109122
public void TestStarted(ITestAdaptor test)
110123
{
111-
// Optional: Add test started tracking if needed
124+
if (test?.Name == null) return;
125+
Debug.Log($"[MCP Unity] Starting test: {test.Name}");
112126
}
113127

114128
public void TestFinished(ITestResultAdaptor result)
115129
{
116-
if (result.TestStatus == TestStatus.Passed)
130+
if (result?.Test == null) return;
131+
132+
try
117133
{
118-
_passCount++;
134+
string status = result.TestStatus.ToString();
135+
Debug.Log($"[MCP Unity] Test finished: {result.Test.Name} - {status}");
136+
137+
if (result.TestStatus == TestStatus.Passed)
138+
{
139+
_passCount++;
140+
}
141+
else if (result.TestStatus == TestStatus.Failed)
142+
{
143+
_failCount++;
144+
}
145+
146+
if (_testResults != null)
147+
{
148+
_testResults.Add(new JObject
149+
{
150+
["name"] = result.Test?.Name ?? "Unknown Test",
151+
["status"] = status,
152+
["message"] = result.Message ?? string.Empty,
153+
["duration"] = result.Duration
154+
});
155+
}
119156
}
120-
else if (result.TestStatus == TestStatus.Failed)
157+
catch (Exception ex)
121158
{
122-
_failCount++;
159+
Debug.LogError($"[MCP Unity] Error in TestFinished: {ex.Message}");
123160
}
124-
125-
_testResults.Add(new JObject
126-
{
127-
["name"] = result.Test.Name,
128-
["status"] = result.TestStatus.ToString(),
129-
["message"] = result.Message,
130-
["duration"] = result.Duration
131-
});
132161
}
133162
}
134163
}

Editor/Tools/SelectGameObjectTool.cs

+40-57
Original file line numberDiff line numberDiff line change
@@ -24,76 +24,59 @@ public SelectGameObjectTool()
2424
/// <param name="parameters">Tool parameters as a JObject</param>
2525
public override JObject Execute(JObject parameters)
2626
{
27-
GameObject targetObject = null;
28-
29-
// Try to find by instance ID first
30-
if (parameters["instanceId"] != null)
27+
// Extract parameters
28+
string objectPath = parameters["objectPath"]?.ToObject<string>();
29+
int? instanceId = parameters["instanceId"]?.ToObject<int?>();
30+
31+
// Validate parameters - require either objectPath or instanceId
32+
if (string.IsNullOrEmpty(objectPath) && !instanceId.HasValue)
3133
{
32-
int instanceId = parameters["instanceId"].ToObject<int>();
33-
targetObject = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
34-
35-
if (targetObject == null)
36-
{
37-
return McpUnitySocketHandler.CreateErrorResponse(
38-
$"GameObject with instance ID {instanceId} not found",
39-
"object_not_found"
40-
);
41-
}
34+
return McpUnitySocketHandler.CreateErrorResponse(
35+
"Required parameter 'objectPath' or 'instanceId' not provided",
36+
"validation_error"
37+
);
4238
}
43-
// Then try by path/name
44-
else if (parameters["objectPath"] != null)
45-
{
46-
string objectPath = parameters["objectPath"].ToObject<string>();
47-
48-
if (string.IsNullOrEmpty(objectPath))
49-
{
50-
return McpUnitySocketHandler.CreateErrorResponse(
51-
"Object path cannot be empty",
52-
"validation_error"
53-
);
54-
}
55-
56-
// Try to find by full path first
57-
targetObject = GameObject.Find(objectPath);
58-
59-
// If not found, try to find by name
60-
if (targetObject == null)
61-
{
62-
var allObjects = UnityEngine.Resources.FindObjectsOfTypeAll<GameObject>();
63-
foreach (var obj in allObjects)
64-
{
65-
if (obj.name == objectPath)
66-
{
67-
targetObject = obj;
68-
break;
69-
}
70-
}
71-
}
39+
40+
GameObject foundObject = null;
41+
string identifier = "";
7242

73-
if (targetObject == null)
74-
{
75-
return McpUnitySocketHandler.CreateErrorResponse(
76-
$"GameObject with path or name '{objectPath}' not found",
77-
"object_not_found"
78-
);
79-
}
43+
// First try to find by instance ID if provided
44+
if (instanceId.HasValue)
45+
{
46+
foundObject = EditorUtility.InstanceIDToObject(instanceId.Value) as GameObject;
47+
identifier = $"instance ID {instanceId.Value}";
8048
}
49+
// Otherwise, try to find by object path/name if provided
8150
else
51+
{
52+
// Try to find the object by path in the hierarchy
53+
foundObject = GameObject.Find(objectPath);
54+
identifier = $"path '{objectPath}'";
55+
}
56+
57+
// Check if we actually found the object
58+
if (foundObject == null)
8259
{
8360
return McpUnitySocketHandler.CreateErrorResponse(
84-
"Either instanceId or objectPath must be provided",
85-
"validation_error"
61+
$"GameObject with {identifier} not found",
62+
"not_found_error"
8663
);
8764
}
8865

89-
// Select the object
90-
Selection.activeGameObject = targetObject;
91-
66+
// Set the selection and ping the object
67+
Selection.activeGameObject = foundObject;
68+
EditorGUIUtility.PingObject(foundObject);
69+
70+
// Log the selection
71+
Debug.Log($"[MCP Unity] Selected GameObject: {foundObject.name} (found by {identifier})");
72+
73+
// Create the response with instanceId for tracking
9274
return new JObject
9375
{
9476
["success"] = true,
95-
["message"] = $"Successfully selected GameObject: {targetObject.name}",
96-
["instanceId"] = targetObject.GetInstanceID()
77+
["message"] = $"Successfully selected GameObject: {foundObject.name}",
78+
["type"] = "text",
79+
["instanceId"] = foundObject.GetInstanceID()
9780
};
9881
}
9982
}

Editor/UnityBridge/McpUnitySocketHandler.cs

+9-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,15 @@ protected override async void OnMessage(MessageEventArgs e)
9999
Debug.Log($"[MCP Unity] WebSocket message response: {responseStr}");
100100

101101
// Send the response back to the client
102-
Send(responseStr);
102+
try
103+
{
104+
Send(responseStr);
105+
}
106+
catch (Exception sendEx)
107+
{
108+
Debug.LogError($"[MCP Unity] Failed to send successful response to client (Request ID: {requestId}): {sendEx.Message}\nResponse was: {responseStr}");
109+
// Optionally: Rethrow or handle differently if needed, but avoid sending another message if connection is likely broken.
110+
}
103111
}
104112
catch (Exception ex)
105113
{

0 commit comments

Comments
 (0)