diff --git a/MCPForUnity/Editor/Services/ITestRunnerService.cs b/MCPForUnity/Editor/Services/ITestRunnerService.cs
index 575e4d9..c24d9e4 100644
--- a/MCPForUnity/Editor/Services/ITestRunnerService.cs
+++ b/MCPForUnity/Editor/Services/ITestRunnerService.cs
@@ -4,6 +4,33 @@ using UnityEditor.TestTools.TestRunner.Api;
namespace MCPForUnity.Editor.Services
{
+ ///
+ /// Options for filtering which tests to run.
+ /// All properties are optional - null or empty arrays are ignored.
+ ///
+ public class TestFilterOptions
+ {
+ ///
+ /// Full names of specific tests to run (e.g., "MyNamespace.MyTests.TestMethod").
+ ///
+ public string[] TestNames { get; set; }
+
+ ///
+ /// Same as TestNames, except it allows for Regex.
+ ///
+ public string[] GroupNames { get; set; }
+
+ ///
+ /// NUnit category names to filter by (tests marked with [Category] attribute).
+ ///
+ public string[] CategoryNames { get; set; }
+
+ ///
+ /// Assembly names to filter tests by.
+ ///
+ public string[] AssemblyNames { get; set; }
+ }
+
///
/// Provides access to Unity Test Runner data and execution.
///
@@ -16,8 +43,10 @@ namespace MCPForUnity.Editor.Services
Task>> GetTestsAsync(TestMode? mode);
///
- /// Execute tests for the supplied mode.
+ /// Execute tests for the supplied mode with optional filtering.
///
- Task RunTestsAsync(TestMode mode);
+ /// The test mode (EditMode or PlayMode).
+ /// Optional filter options to run specific tests. Pass null to run all tests.
+ Task RunTestsAsync(TestMode mode, TestFilterOptions filterOptions = null);
}
}
diff --git a/MCPForUnity/Editor/Services/TestRunnerService.cs b/MCPForUnity/Editor/Services/TestRunnerService.cs
index f8cbd13..2ec9084 100644
--- a/MCPForUnity/Editor/Services/TestRunnerService.cs
+++ b/MCPForUnity/Editor/Services/TestRunnerService.cs
@@ -58,7 +58,7 @@ namespace MCPForUnity.Editor.Services
}
}
- public async Task RunTestsAsync(TestMode mode)
+ public async Task RunTestsAsync(TestMode mode, TestFilterOptions filterOptions = null)
{
await _operationLock.WaitAsync().ConfigureAwait(true);
Task runTask;
@@ -94,7 +94,14 @@ namespace MCPForUnity.Editor.Services
_leafResults.Clear();
_runCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- var filter = new Filter { testMode = mode };
+ var filter = new Filter
+ {
+ testMode = mode,
+ testNames = filterOptions?.TestNames,
+ groupNames = filterOptions?.GroupNames,
+ categoryNames = filterOptions?.CategoryNames,
+ assemblyNames = filterOptions?.AssemblyNames
+ };
var settings = new ExecutionSettings(filter);
if (mode == TestMode.PlayMode)
diff --git a/MCPForUnity/Editor/Tools/RunTests.cs b/MCPForUnity/Editor/Tools/RunTests.cs
index f49e57f..b40df25 100644
--- a/MCPForUnity/Editor/Tools/RunTests.cs
+++ b/MCPForUnity/Editor/Tools/RunTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using System.Threading.Tasks;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Resources.Tests;
@@ -42,11 +43,13 @@ namespace MCPForUnity.Editor.Tools
// Preserve default timeout if parsing fails
}
+ var filterOptions = ParseFilterOptions(@params);
+
var testService = MCPServiceLocator.Tests;
Task runTask;
try
{
- runTask = testService.RunTestsAsync(parsedMode.Value);
+ runTask = testService.RunTestsAsync(parsedMode.Value, filterOptions);
}
catch (Exception ex)
{
@@ -69,5 +72,66 @@ namespace MCPForUnity.Editor.Tools
var data = result.ToSerializable(parsedMode.Value.ToString());
return new SuccessResponse(message, data);
}
+
+ private static TestFilterOptions ParseFilterOptions(JObject @params)
+ {
+ if (@params == null)
+ {
+ return null;
+ }
+
+ var testNames = ParseStringArray(@params, "testNames");
+ var groupNames = ParseStringArray(@params, "groupNames");
+ var categoryNames = ParseStringArray(@params, "categoryNames");
+ var assemblyNames = ParseStringArray(@params, "assemblyNames");
+
+ // Return null if no filters specified
+ if (testNames == null && groupNames == null && categoryNames == null && assemblyNames == null)
+ {
+ return null;
+ }
+
+ return new TestFilterOptions
+ {
+ TestNames = testNames,
+ GroupNames = groupNames,
+ CategoryNames = categoryNames,
+ AssemblyNames = assemblyNames
+ };
+ }
+
+ private static string[] ParseStringArray(JObject @params, string key)
+ {
+ var token = @params[key];
+ if (token == null)
+ {
+ return null;
+ }
+
+ if (token.Type == JTokenType.String)
+ {
+ var value = token.ToString();
+ return string.IsNullOrWhiteSpace(value) ? null : new[] { value };
+ }
+
+ if (token.Type == JTokenType.Array)
+ {
+ var array = token as JArray;
+ if (array == null || array.Count == 0)
+ {
+ return null;
+ }
+
+ var values = array
+ .Where(t => t.Type == JTokenType.String)
+ .Select(t => t.ToString())
+ .Where(s => !string.IsNullOrWhiteSpace(s))
+ .ToArray();
+
+ return values.Length > 0 ? values : null;
+ }
+
+ return null;
+ }
}
}
diff --git a/Server/src/services/tools/run_tests.py b/Server/src/services/tools/run_tests.py
index 520a9db..7d49daa 100644
--- a/Server/src/services/tools/run_tests.py
+++ b/Server/src/services/tools/run_tests.py
@@ -45,10 +45,12 @@ class RunTestsResponse(MCPResponse):
)
async def run_tests(
ctx: Context,
- mode: Annotated[Literal["EditMode", "PlayMode"], Field(
- description="Unity test mode to run")] = "EditMode",
- timeout_seconds: Annotated[int | str, Field(
- description="Optional timeout in seconds for the Unity test run (string, e.g. '30')")] | None = None,
+ mode: Annotated[Literal["EditMode", "PlayMode"], "Unity test mode to run"] = "EditMode",
+ timeout_seconds: Annotated[int | str, "Optional timeout in seconds for the test run"] | None = None,
+ test_names: Annotated[list[str] | str, "Full names of specific tests to run (e.g., 'MyNamespace.MyTests.TestMethod')"] | None = None,
+ group_names: Annotated[list[str] | str, "Same as test_names, except it allows for Regex"] | None = None,
+ category_names: Annotated[list[str] | str, "NUnit category names to filter by (tests marked with [Category] attribute)"] | None = None,
+ assembly_names: Annotated[list[str] | str, "Assembly names to filter tests by"] | None = None,
) -> RunTestsResponse:
unity_instance = get_unity_instance_from_context(ctx)
@@ -68,11 +70,39 @@ async def run_tests(
except Exception:
return default
+ # Coerce string or list to list of strings
+ def _coerce_string_list(value) -> list[str] | None:
+ if value is None:
+ return None
+ if isinstance(value, str):
+ return [value] if value.strip() else None
+ if isinstance(value, list):
+ result = [str(v).strip() for v in value if v and str(v).strip()]
+ return result if result else None
+ return None
+
params: dict[str, Any] = {"mode": mode}
ts = _coerce_int(timeout_seconds)
if ts is not None:
params["timeoutSeconds"] = ts
+ # Add filter parameters if provided
+ test_names_list = _coerce_string_list(test_names)
+ if test_names_list:
+ params["testNames"] = test_names_list
+
+ group_names_list = _coerce_string_list(group_names)
+ if group_names_list:
+ params["groupNames"] = group_names_list
+
+ category_names_list = _coerce_string_list(category_names)
+ if category_names_list:
+ params["categoryNames"] = category_names_list
+
+ assembly_names_list = _coerce_string_list(assembly_names)
+ if assembly_names_list:
+ params["assemblyNames"] = assembly_names_list
+
response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "run_tests", params)
await ctx.info(f'Response {response}')
return RunTestsResponse(**response) if isinstance(response, dict) else response