升级XR插件版本

This commit is contained in:
Sora丶kong
2026-03-02 17:56:21 +08:00
parent 8962657674
commit 60f512a9bc
1317 changed files with 110305 additions and 48249 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 266967ec2e1df44209bf46ec6037d61d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,64 @@
using System;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
namespace MCPForUnity.Editor.Resources.Editor
{
/// <summary>
/// Provides information about the currently active editor tool.
/// </summary>
[McpForUnityResource("get_active_tool")]
public static class ActiveTool
{
public static object HandleCommand(JObject @params)
{
try
{
Tool currentTool = UnityEditor.Tools.current;
string toolName = currentTool.ToString();
bool customToolActive = UnityEditor.Tools.current == Tool.Custom;
string activeToolName = customToolActive ? EditorTools.GetActiveToolName() : toolName;
var toolInfo = new
{
activeTool = activeToolName,
isCustom = customToolActive,
pivotMode = UnityEditor.Tools.pivotMode.ToString(),
pivotRotation = UnityEditor.Tools.pivotRotation.ToString(),
handleRotation = new
{
x = UnityEditor.Tools.handleRotation.eulerAngles.x,
y = UnityEditor.Tools.handleRotation.eulerAngles.y,
z = UnityEditor.Tools.handleRotation.eulerAngles.z
},
handlePosition = new
{
x = UnityEditor.Tools.handlePosition.x,
y = UnityEditor.Tools.handlePosition.y,
z = UnityEditor.Tools.handlePosition.z
}
};
return new SuccessResponse("Retrieved active tool information.", toolInfo);
}
catch (Exception e)
{
return new ErrorResponse($"Error getting active tool: {e.Message}");
}
}
}
// Helper class for custom tool names
internal static class EditorTools
{
public static string GetActiveToolName()
{
if (UnityEditor.Tools.current == Tool.Custom)
{
return "Unknown Custom Tool";
}
return UnityEditor.Tools.current.ToString();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6e78b6227ab7742a8a4f679ee6a8a212
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,27 @@
using System;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using Newtonsoft.Json.Linq;
namespace MCPForUnity.Editor.Resources.Editor
{
/// <summary>
/// Provides dynamic editor state information that changes frequently.
/// </summary>
[McpForUnityResource("get_editor_state")]
public static class EditorState
{
public static object HandleCommand(JObject @params)
{
try
{
var snapshot = EditorStateCache.GetSnapshot();
return new SuccessResponse("Retrieved editor state.", snapshot);
}
catch (Exception e)
{
return new ErrorResponse($"Error getting editor state: {e.Message}");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7c6df54e014c44fdb0cd3f65a479e37
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
using System;
using System.Linq;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
namespace MCPForUnity.Editor.Resources.Editor
{
/// <summary>
/// Provides detailed information about the current editor selection.
/// </summary>
[McpForUnityResource("get_selection")]
public static class Selection
{
public static object HandleCommand(JObject @params)
{
try
{
var selectionInfo = new
{
activeObject = UnityEditor.Selection.activeObject?.name,
activeGameObject = UnityEditor.Selection.activeGameObject?.name,
activeTransform = UnityEditor.Selection.activeTransform?.name,
activeInstanceID = UnityEditor.Selection.activeInstanceID,
count = UnityEditor.Selection.count,
objects = UnityEditor.Selection.objects
.Select(obj => new
{
name = obj?.name,
type = obj?.GetType().FullName,
instanceID = obj?.GetInstanceID()
})
.ToList(),
gameObjects = UnityEditor.Selection.gameObjects
.Select(go => new
{
name = go?.name,
instanceID = go?.GetInstanceID()
})
.ToList(),
assetGUIDs = UnityEditor.Selection.assetGUIDs
};
return new SuccessResponse("Retrieved current selection details.", selectionInfo);
}
catch (Exception e)
{
return new ErrorResponse($"Error getting selection: {e.Message}");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7ea869623e094599a70be086ab4fc0e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Resources.Editor
{
/// <summary>
/// Provides list of all open editor windows.
/// </summary>
[McpForUnityResource("get_windows")]
public static class Windows
{
public static object HandleCommand(JObject @params)
{
try
{
EditorWindow[] allWindows = UnityEngine.Resources.FindObjectsOfTypeAll<EditorWindow>();
var openWindows = new List<object>();
foreach (EditorWindow window in allWindows)
{
if (window == null)
continue;
try
{
openWindows.Add(new
{
title = window.titleContent.text,
typeName = window.GetType().FullName,
isFocused = EditorWindow.focusedWindow == window,
position = new
{
x = window.position.x,
y = window.position.y,
width = window.position.width,
height = window.position.height
},
instanceID = window.GetInstanceID()
});
}
catch (Exception ex)
{
McpLog.Warn($"Could not get info for window {window.GetType().Name}: {ex.Message}");
}
}
return new SuccessResponse("Retrieved list of open editor windows.", openWindows);
}
catch (Exception e)
{
return new ErrorResponse($"Error getting editor windows: {e.Message}");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 58a341e64bea440b29deaf859aaea552
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
using System;
namespace MCPForUnity.Editor.Resources
{
/// <summary>
/// Marks a class as an MCP resource handler for auto-discovery.
/// The class must have a public static HandleCommand(JObject) method.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class McpForUnityResourceAttribute : Attribute
{
/// <summary>
/// The resource name used to route requests to this resource.
/// If not specified, defaults to the PascalCase class name converted to snake_case.
/// </summary>
public string ResourceName { get; }
/// <summary>
/// Human-readable description of what this resource provides.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Create an MCP resource attribute with auto-generated resource name.
/// The resource name will be derived from the class name (PascalCase → snake_case).
/// Example: ManageAsset → manage_asset
/// </summary>
public McpForUnityResourceAttribute()
{
ResourceName = null; // Will be auto-generated
}
/// <summary>
/// Create an MCP resource attribute with explicit resource name.
/// </summary>
/// <param name="resourceName">The resource name (e.g., "manage_asset")</param>
public McpForUnityResourceAttribute(string resourceName)
{
ResourceName = resourceName;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4c2d60f570f3d4bd2a6a2c1293094be3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bca79cd3ef8ed466f9e50e2dc7850e46
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
namespace MCPForUnity.Editor.Resources.MenuItems
{
/// <summary>
/// Provides a simple read-only resource that returns Unity menu items.
/// </summary>
[McpForUnityResource("get_menu_items")]
public static class GetMenuItems
{
private static List<string> _cached;
[InitializeOnLoadMethod]
private static void BuildCache() => Refresh();
public static object HandleCommand(JObject @params)
{
bool forceRefresh = @params?["refresh"]?.ToObject<bool>() ?? false;
string search = @params?["search"]?.ToString();
var items = GetMenuItemsInternal(forceRefresh);
if (!string.IsNullOrEmpty(search))
{
items = items
.Where(item => item.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0)
.ToList();
}
string message = $"Retrieved {items.Count} menu items";
return new SuccessResponse(message, items);
}
internal static List<string> GetMenuItemsInternal(bool forceRefresh)
{
if (forceRefresh || _cached == null)
{
Refresh();
}
return (_cached ?? new List<string>()).ToList();
}
private static void Refresh()
{
try
{
var methods = TypeCache.GetMethodsWithAttribute<MenuItem>();
_cached = methods
.SelectMany(m => m
.GetCustomAttributes(typeof(MenuItem), false)
.OfType<MenuItem>()
.Select(attr => attr.menuItem))
.Where(s => !string.IsNullOrEmpty(s))
.Distinct(StringComparer.Ordinal)
.OrderBy(s => s, StringComparer.Ordinal)
.ToList();
}
catch (Exception ex)
{
McpLog.Error($"[GetMenuItems] Failed to scan menu items: {ex}");
_cached ??= new List<string>();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 04eeea61eb5c24033a88013845d25f23
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 538489f13d7914c4eba9a67e29001b43
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace MCPForUnity.Editor.Resources.Project
{
/// <summary>
/// Provides dictionary of layer indices to layer names.
/// </summary>
[McpForUnityResource("get_layers")]
public static class Layers
{
private const int TotalLayerCount = 32;
public static object HandleCommand(JObject @params)
{
try
{
var layers = new Dictionary<int, string>();
for (int i = 0; i < TotalLayerCount; i++)
{
string layerName = LayerMask.LayerToName(i);
if (!string.IsNullOrEmpty(layerName))
{
layers.Add(i, layerName);
}
}
return new SuccessResponse("Retrieved current named layers.", layers);
}
catch (Exception e)
{
return new ErrorResponse($"Failed to retrieve layers: {e.Message}");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 959ee428299454ac19a636275208ca00
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,41 @@
using System;
using System.IO;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Resources.Project
{
/// <summary>
/// Provides static project configuration information.
/// </summary>
[McpForUnityResource("get_project_info")]
public static class ProjectInfo
{
public static object HandleCommand(JObject @params)
{
try
{
string assetsPath = Application.dataPath.Replace('\\', '/');
string projectRoot = Directory.GetParent(assetsPath)?.FullName.Replace('\\', '/');
string projectName = Path.GetFileName(projectRoot);
var info = new
{
projectRoot = projectRoot ?? "",
projectName = projectName ?? "",
unityVersion = Application.unityVersion,
platform = EditorUserBuildSettings.activeBuildTarget.ToString(),
assetsPath = assetsPath
};
return new SuccessResponse("Retrieved project info.", info);
}
catch (Exception e)
{
return new ErrorResponse($"Error getting project info: {e.Message}");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81b03415fcf93466e9ed667d19b58d43
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,27 @@
using System;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditorInternal;
namespace MCPForUnity.Editor.Resources.Project
{
/// <summary>
/// Provides list of all tags in the project.
/// </summary>
[McpForUnityResource("get_tags")]
public static class Tags
{
public static object HandleCommand(JObject @params)
{
try
{
string[] tags = InternalEditorUtility.tags;
return new SuccessResponse("Retrieved current tags.", tags);
}
catch (Exception e)
{
return new ErrorResponse($"Failed to retrieve tags: {e.Message}");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2179ac5d98f264d1681e7d5c0d0ed341
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 563f6050485b445449a1db200bfba51c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,284 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Resources.Scene
{
/// <summary>
/// Resource handler for reading GameObject data.
/// Provides read-only access to GameObject information without component serialization.
///
/// URI: unity://scene/gameobject/{instanceID}
/// </summary>
[McpForUnityResource("get_gameobject")]
public static class GameObjectResource
{
public static object HandleCommand(JObject @params)
{
if (@params == null)
{
return new ErrorResponse("Parameters cannot be null.");
}
// Get instance ID from params
int? instanceID = null;
var idToken = @params["instanceID"] ?? @params["instance_id"] ?? @params["id"];
if (idToken != null)
{
instanceID = ParamCoercion.CoerceInt(idToken, -1);
if (instanceID == -1)
{
instanceID = null;
}
}
if (!instanceID.HasValue)
{
return new ErrorResponse("'instanceID' parameter is required.");
}
try
{
var go = EditorUtility.InstanceIDToObject(instanceID.Value) as GameObject;
if (go == null)
{
return new ErrorResponse($"GameObject with instance ID {instanceID} not found.");
}
return new
{
success = true,
data = SerializeGameObject(go)
};
}
catch (Exception e)
{
McpLog.Error($"[GameObjectResource] Error getting GameObject: {e}");
return new ErrorResponse($"Error getting GameObject: {e.Message}");
}
}
/// <summary>
/// Serializes a GameObject without component details.
/// For component data, use GetComponents or GetComponent resources.
/// </summary>
public static object SerializeGameObject(GameObject go)
{
if (go == null)
return null;
var transform = go.transform;
// Get component type names (not full serialization)
var componentTypes = go.GetComponents<Component>()
.Where(c => c != null)
.Select(c => c.GetType().Name)
.ToList();
// Get children instance IDs (not full serialization)
var childrenIds = new List<int>();
foreach (Transform child in transform)
{
childrenIds.Add(child.gameObject.GetInstanceID());
}
return new
{
instanceID = go.GetInstanceID(),
name = go.name,
tag = go.tag,
layer = go.layer,
layerName = LayerMask.LayerToName(go.layer),
active = go.activeSelf,
activeInHierarchy = go.activeInHierarchy,
isStatic = go.isStatic,
transform = new
{
position = SerializeVector3(transform.position),
localPosition = SerializeVector3(transform.localPosition),
rotation = SerializeVector3(transform.eulerAngles),
localRotation = SerializeVector3(transform.localEulerAngles),
scale = SerializeVector3(transform.localScale),
lossyScale = SerializeVector3(transform.lossyScale)
},
parent = transform.parent != null ? transform.parent.gameObject.GetInstanceID() : (int?)null,
children = childrenIds,
componentTypes = componentTypes,
path = GameObjectLookup.GetGameObjectPath(go)
};
}
private static object SerializeVector3(Vector3 v)
{
return new { x = v.x, y = v.y, z = v.z };
}
}
/// <summary>
/// Resource handler for reading all components on a GameObject.
///
/// URI: unity://scene/gameobject/{instanceID}/components
/// </summary>
[McpForUnityResource("get_gameobject_components")]
public static class GameObjectComponentsResource
{
public static object HandleCommand(JObject @params)
{
if (@params == null)
{
return new ErrorResponse("Parameters cannot be null.");
}
var idToken = @params["instanceID"] ?? @params["instance_id"] ?? @params["id"];
int instanceID = ParamCoercion.CoerceInt(idToken, -1);
if (instanceID == -1)
{
return new ErrorResponse("'instanceID' parameter is required.");
}
// Pagination parameters
int pageSize = ParamCoercion.CoerceInt(@params["pageSize"] ?? @params["page_size"], 25);
int cursor = ParamCoercion.CoerceInt(@params["cursor"], 0);
bool includeProperties = ParamCoercion.CoerceBool(@params["includeProperties"] ?? @params["include_properties"], true);
pageSize = Mathf.Clamp(pageSize, 1, 100);
try
{
var go = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
if (go == null)
{
return new ErrorResponse($"GameObject with instance ID {instanceID} not found.");
}
var allComponents = go.GetComponents<Component>().Where(c => c != null).ToList();
int total = allComponents.Count;
var pagedComponents = allComponents.Skip(cursor).Take(pageSize).ToList();
var componentData = new List<object>();
foreach (var component in pagedComponents)
{
if (includeProperties)
{
componentData.Add(GameObjectSerializer.GetComponentData(component));
}
else
{
componentData.Add(new
{
typeName = component.GetType().FullName,
instanceID = component.GetInstanceID()
});
}
}
int? nextCursor = cursor + pagedComponents.Count < total ? cursor + pagedComponents.Count : (int?)null;
return new
{
success = true,
data = new
{
gameObjectID = instanceID,
gameObjectName = go.name,
components = componentData,
cursor = cursor,
pageSize = pageSize,
nextCursor = nextCursor,
totalCount = total,
hasMore = nextCursor.HasValue,
includeProperties = includeProperties
}
};
}
catch (Exception e)
{
McpLog.Error($"[GameObjectComponentsResource] Error getting components: {e}");
return new ErrorResponse($"Error getting components: {e.Message}");
}
}
}
/// <summary>
/// Resource handler for reading a single component on a GameObject.
///
/// URI: unity://scene/gameobject/{instanceID}/component/{componentName}
/// </summary>
[McpForUnityResource("get_gameobject_component")]
public static class GameObjectComponentResource
{
public static object HandleCommand(JObject @params)
{
if (@params == null)
{
return new ErrorResponse("Parameters cannot be null.");
}
var idToken = @params["instanceID"] ?? @params["instance_id"] ?? @params["id"];
int instanceID = ParamCoercion.CoerceInt(idToken, -1);
if (instanceID == -1)
{
return new ErrorResponse("'instanceID' parameter is required.");
}
string componentName = ParamCoercion.CoerceString(@params["componentName"] ?? @params["component_name"] ?? @params["component"], null);
if (string.IsNullOrEmpty(componentName))
{
return new ErrorResponse("'componentName' parameter is required.");
}
try
{
var go = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
if (go == null)
{
return new ErrorResponse($"GameObject with instance ID {instanceID} not found.");
}
// Find the component by type name
Component targetComponent = null;
foreach (var component in go.GetComponents<Component>())
{
if (component == null) continue;
var typeName = component.GetType().Name;
var fullTypeName = component.GetType().FullName;
if (string.Equals(typeName, componentName, StringComparison.OrdinalIgnoreCase) ||
string.Equals(fullTypeName, componentName, StringComparison.OrdinalIgnoreCase))
{
targetComponent = component;
break;
}
}
if (targetComponent == null)
{
return new ErrorResponse($"Component '{componentName}' not found on GameObject '{go.name}'.");
}
return new
{
success = true,
data = new
{
gameObjectID = instanceID,
gameObjectName = go.name,
component = GameObjectSerializer.GetComponentData(targetComponent)
}
};
}
catch (Exception e)
{
McpLog.Error($"[GameObjectComponentResource] Error getting component: {e}");
return new ErrorResponse($"Error getting component: {e.Message}");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ee79050d9f6d42798a0757cc7672517
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 412726d2e774048939b0d2bd4f11a503
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,217 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using Newtonsoft.Json.Linq;
using UnityEditor.TestTools.TestRunner.Api;
namespace MCPForUnity.Editor.Resources.Tests
{
/// <summary>
/// Provides access to Unity tests from the Test Framework with pagination and filtering support.
/// This is a read-only resource that can be queried by MCP clients.
///
/// Parameters:
/// - mode (optional): Filter by "EditMode" or "PlayMode"
/// - filter (optional): Filter test names by pattern (case-insensitive contains)
/// - page_size (optional): Number of tests per page (default: 50, max: 200)
/// - cursor (optional): 0-based cursor for pagination
/// - page_number (optional): 1-based page number (converted to cursor)
/// </summary>
[McpForUnityResource("get_tests")]
public static class GetTests
{
private const int DEFAULT_PAGE_SIZE = 50;
private const int MAX_PAGE_SIZE = 200;
public static async Task<object> HandleCommand(JObject @params)
{
// Parse mode filter
TestMode? modeFilter = null;
string modeStr = @params?["mode"]?.ToString();
if (!string.IsNullOrEmpty(modeStr))
{
if (!ModeParser.TryParse(modeStr, out modeFilter, out var parseError))
{
return new ErrorResponse(parseError);
}
}
// Parse name filter
string nameFilter = @params?["filter"]?.ToString();
McpLog.Info($"[GetTests] Retrieving tests (mode={modeFilter?.ToString() ?? "all"}, filter={nameFilter ?? "none"})");
IReadOnlyList<Dictionary<string, string>> allTests;
try
{
allTests = await MCPServiceLocator.Tests.GetTestsAsync(modeFilter).ConfigureAwait(true);
}
catch (Exception ex)
{
McpLog.Error($"[GetTests] Error retrieving tests: {ex.Message}\n{ex.StackTrace}");
return new ErrorResponse("Failed to retrieve tests");
}
// Apply name filter if provided and convert to List for pagination
List<Dictionary<string, string>> filteredTests;
if (!string.IsNullOrEmpty(nameFilter))
{
filteredTests = allTests
.Where(t =>
(t.ContainsKey("name") && t["name"].IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0) ||
(t.ContainsKey("full_name") && t["full_name"].IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0)
)
.ToList();
}
else
{
filteredTests = allTests.ToList();
}
// Clamp page_size before parsing pagination to ensure cursor is computed correctly
int requestedPageSize = ParamCoercion.CoerceInt(
@params?["page_size"] ?? @params?["pageSize"],
DEFAULT_PAGE_SIZE
);
int clampedPageSize = System.Math.Min(requestedPageSize, MAX_PAGE_SIZE);
if (clampedPageSize <= 0) clampedPageSize = DEFAULT_PAGE_SIZE;
// Create modified params with clamped page_size for cursor calculation
var paginationParams = new JObject(@params);
paginationParams["page_size"] = clampedPageSize;
// Parse pagination with clamped page size
var pagination = PaginationRequest.FromParams(paginationParams, DEFAULT_PAGE_SIZE);
// Create paginated response
var response = PaginationResponse<Dictionary<string, string>>.Create(filteredTests, pagination);
string message = !string.IsNullOrEmpty(nameFilter)
? $"Retrieved {response.Items.Count} of {response.TotalCount} tests matching '{nameFilter}' (cursor {response.Cursor})"
: $"Retrieved {response.Items.Count} of {response.TotalCount} tests (cursor {response.Cursor})";
return new SuccessResponse(message, response);
}
}
/// <summary>
/// DEPRECATED: Use get_tests with mode parameter instead.
/// Provides access to Unity tests for a specific mode (EditMode or PlayMode).
/// This is a read-only resource that can be queried by MCP clients.
///
/// Parameters:
/// - mode (required): "EditMode" or "PlayMode"
/// - filter (optional): Filter test names by pattern (case-insensitive contains)
/// - page_size (optional): Number of tests per page (default: 50, max: 200)
/// - cursor (optional): 0-based cursor for pagination
/// </summary>
[McpForUnityResource("get_tests_for_mode")]
public static class GetTestsForMode
{
private const int DEFAULT_PAGE_SIZE = 50;
private const int MAX_PAGE_SIZE = 200;
public static async Task<object> HandleCommand(JObject @params)
{
string modeStr = @params?["mode"]?.ToString();
if (string.IsNullOrEmpty(modeStr))
{
return new ErrorResponse("'mode' parameter is required");
}
if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError))
{
return new ErrorResponse(parseError);
}
// Parse name filter
string nameFilter = @params?["filter"]?.ToString();
McpLog.Info($"[GetTestsForMode] Retrieving tests for mode: {parsedMode.Value} (filter={nameFilter ?? "none"})");
IReadOnlyList<Dictionary<string, string>> allTests;
try
{
allTests = await MCPServiceLocator.Tests.GetTestsAsync(parsedMode).ConfigureAwait(true);
}
catch (Exception ex)
{
McpLog.Error($"[GetTestsForMode] Error retrieving tests: {ex.Message}\n{ex.StackTrace}");
return new ErrorResponse("Failed to retrieve tests");
}
// Apply name filter if provided and convert to List for pagination
List<Dictionary<string, string>> filteredTests;
if (!string.IsNullOrEmpty(nameFilter))
{
filteredTests = allTests
.Where(t =>
(t.ContainsKey("name") && t["name"].IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0) ||
(t.ContainsKey("full_name") && t["full_name"].IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0)
)
.ToList();
}
else
{
filteredTests = allTests.ToList();
}
// Clamp page_size before parsing pagination to ensure cursor is computed correctly
int requestedPageSize = ParamCoercion.CoerceInt(
@params?["page_size"] ?? @params?["pageSize"],
DEFAULT_PAGE_SIZE
);
int clampedPageSize = System.Math.Min(requestedPageSize, MAX_PAGE_SIZE);
if (clampedPageSize <= 0) clampedPageSize = DEFAULT_PAGE_SIZE;
// Create modified params with clamped page_size for cursor calculation
var paginationParams = new JObject(@params);
paginationParams["page_size"] = clampedPageSize;
// Parse pagination with clamped page size
var pagination = PaginationRequest.FromParams(paginationParams, DEFAULT_PAGE_SIZE);
// Create paginated response
var response = PaginationResponse<Dictionary<string, string>>.Create(filteredTests, pagination);
string message = nameFilter != null
? $"Retrieved {response.Items.Count} of {response.TotalCount} {parsedMode.Value} tests matching '{nameFilter}'"
: $"Retrieved {response.Items.Count} of {response.TotalCount} {parsedMode.Value} tests";
return new SuccessResponse(message, response);
}
}
internal static class ModeParser
{
internal static bool TryParse(string modeStr, out TestMode? mode, out string error)
{
error = null;
mode = null;
if (string.IsNullOrWhiteSpace(modeStr))
{
error = "'mode' parameter cannot be empty";
return false;
}
if (modeStr.Equals("EditMode", StringComparison.OrdinalIgnoreCase))
{
mode = TestMode.EditMode;
return true;
}
if (modeStr.Equals("PlayMode", StringComparison.OrdinalIgnoreCase))
{
mode = TestMode.PlayMode;
return true;
}
error = $"Unknown test mode: '{modeStr}'. Use 'EditMode' or 'PlayMode'";
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 84183aaed077e4f25968269c952db2d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: