diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index a08649e3f..8caf43082 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -43,6 +43,8 @@ internal static class EditorPrefKeys internal const string AutoRegisterEnabled = "MCPForUnity.AutoRegisterEnabled"; internal const string ToolEnabledPrefix = "MCPForUnity.ToolEnabled."; internal const string ToolFoldoutStatePrefix = "MCPForUnity.ToolFoldout."; + internal const string ResourceEnabledPrefix = "MCPForUnity.ResourceEnabled."; + internal const string ResourceFoldoutStatePrefix = "MCPForUnity.ResourceFoldout."; internal const string EditorWindowActivePanel = "MCPForUnity.EditorWindow.ActivePanel"; internal const string SetupCompleted = "MCPForUnity.SetupCompleted"; diff --git a/MCPForUnity/Editor/Helpers/StringCaseUtility.cs b/MCPForUnity/Editor/Helpers/StringCaseUtility.cs index 6437b3c24..04b498a5d 100644 --- a/MCPForUnity/Editor/Helpers/StringCaseUtility.cs +++ b/MCPForUnity/Editor/Helpers/StringCaseUtility.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Text.RegularExpressions; @@ -10,6 +11,28 @@ namespace MCPForUnity.Editor.Helpers /// public static class StringCaseUtility { + /// + /// Checks whether a type belongs to the built-in MCP for Unity package. + /// Returns true when the type's namespace starts with + /// or its assembly is MCPForUnity.Editor. + /// + public static bool IsBuiltInMcpType(Type type, string assemblyName, string builtInNamespacePrefix) + { + if (type != null && !string.IsNullOrEmpty(type.Namespace) + && type.Namespace.StartsWith(builtInNamespacePrefix, StringComparison.Ordinal)) + { + return true; + } + + if (!string.IsNullOrEmpty(assemblyName) + && assemblyName.Equals("MCPForUnity.Editor", StringComparison.Ordinal)) + { + return true; + } + + return false; + } + /// /// Converts a camelCase string to snake_case. /// Example: "searchMethod" -> "search_method", "param1Value" -> "param1_value" diff --git a/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs b/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs index 9b895e230..f86cd5ffd 100644 --- a/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs +++ b/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs @@ -15,6 +15,11 @@ public class McpForUnityResourceAttribute : Attribute /// public string ResourceName { get; } + /// + /// Human-readable description of what this resource provides. + /// + public string Description { get; set; } + /// /// Create an MCP resource attribute with auto-generated resource name. /// The resource name will be derived from the class name (PascalCase → snake_case). diff --git a/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs b/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs new file mode 100644 index 000000000..6595fc8ab --- /dev/null +++ b/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Metadata for a discovered resource + /// + public class ResourceMetadata + { + public string Name { get; set; } + public string Description { get; set; } + public string ClassName { get; set; } + public string Namespace { get; set; } + public string AssemblyName { get; set; } + public bool IsBuiltIn { get; set; } + } + + /// + /// Service for discovering MCP resources via reflection + /// + public interface IResourceDiscoveryService + { + /// + /// Discovers all resources marked with [McpForUnityResource] + /// + List DiscoverAllResources(); + + /// + /// Gets metadata for a specific resource + /// + ResourceMetadata GetResourceMetadata(string resourceName); + + /// + /// Returns only the resources currently enabled + /// + List GetEnabledResources(); + + /// + /// Checks whether a resource is currently enabled + /// + bool IsResourceEnabled(string resourceName); + + /// + /// Updates the enabled state for a resource + /// + void SetResourceEnabled(string resourceName, bool enabled); + + /// + /// Invalidates the resource discovery cache + /// + void InvalidateCache(); + } +} diff --git a/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs.meta b/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs.meta new file mode 100644 index 000000000..171d2b271 --- /dev/null +++ b/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7afb4739669224c74b4b4d706e6bbb49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/MCPServiceLocator.cs b/MCPForUnity/Editor/Services/MCPServiceLocator.cs index ae3d98fbd..c8ceb4d57 100644 --- a/MCPForUnity/Editor/Services/MCPServiceLocator.cs +++ b/MCPForUnity/Editor/Services/MCPServiceLocator.cs @@ -17,6 +17,7 @@ public static class MCPServiceLocator private static IPackageUpdateService _packageUpdateService; private static IPlatformService _platformService; private static IToolDiscoveryService _toolDiscoveryService; + private static IResourceDiscoveryService _resourceDiscoveryService; private static IServerManagementService _serverManagementService; private static TransportManager _transportManager; private static IPackageDeploymentService _packageDeploymentService; @@ -28,6 +29,7 @@ public static class MCPServiceLocator public static IPackageUpdateService Updates => _packageUpdateService ??= new PackageUpdateService(); public static IPlatformService Platform => _platformService ??= new PlatformService(); public static IToolDiscoveryService ToolDiscovery => _toolDiscoveryService ??= new ToolDiscoveryService(); + public static IResourceDiscoveryService ResourceDiscovery => _resourceDiscoveryService ??= new ResourceDiscoveryService(); public static IServerManagementService Server => _serverManagementService ??= new ServerManagementService(); public static TransportManager TransportManager => _transportManager ??= new TransportManager(); public static IPackageDeploymentService Deployment => _packageDeploymentService ??= new PackageDeploymentService(); @@ -53,6 +55,8 @@ public static void Register(T implementation) where T : class _platformService = ps; else if (implementation is IToolDiscoveryService td) _toolDiscoveryService = td; + else if (implementation is IResourceDiscoveryService rd) + _resourceDiscoveryService = rd; else if (implementation is IServerManagementService sm) _serverManagementService = sm; else if (implementation is IPackageDeploymentService pd) @@ -73,6 +77,7 @@ public static void Reset() (_packageUpdateService as IDisposable)?.Dispose(); (_platformService as IDisposable)?.Dispose(); (_toolDiscoveryService as IDisposable)?.Dispose(); + (_resourceDiscoveryService as IDisposable)?.Dispose(); (_serverManagementService as IDisposable)?.Dispose(); (_transportManager as IDisposable)?.Dispose(); (_packageDeploymentService as IDisposable)?.Dispose(); @@ -84,6 +89,7 @@ public static void Reset() _packageUpdateService = null; _platformService = null; _toolDiscoveryService = null; + _resourceDiscoveryService = null; _serverManagementService = null; _transportManager = null; _packageDeploymentService = null; diff --git a/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs b/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs new file mode 100644 index 000000000..d96425961 --- /dev/null +++ b/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using MCPForUnity.Editor.Constants; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Resources; +using UnityEditor; + +namespace MCPForUnity.Editor.Services +{ + public class ResourceDiscoveryService : IResourceDiscoveryService + { + private Dictionary _cachedResources; + + public List DiscoverAllResources() + { + if (_cachedResources != null) + { + return _cachedResources.Values.ToList(); + } + + _cachedResources = new Dictionary(); + + var resourceTypes = TypeCache.GetTypesWithAttribute(); + foreach (var type in resourceTypes) + { + McpForUnityResourceAttribute resourceAttr; + try + { + resourceAttr = type.GetCustomAttribute(); + } + catch (Exception ex) + { + McpLog.Warn($"Failed to read [McpForUnityResource] for {type.FullName}: {ex.Message}"); + continue; + } + + if (resourceAttr == null) + { + continue; + } + + var metadata = ExtractResourceMetadata(type, resourceAttr); + if (metadata != null) + { + if (_cachedResources.ContainsKey(metadata.Name)) + { + McpLog.Warn($"Duplicate resource name '{metadata.Name}' from {type.FullName}; overwriting previous registration."); + } + _cachedResources[metadata.Name] = metadata; + EnsurePreferenceInitialized(metadata); + } + } + + McpLog.Info($"Discovered {_cachedResources.Count} MCP resources via reflection", false); + return _cachedResources.Values.ToList(); + } + + public ResourceMetadata GetResourceMetadata(string resourceName) + { + if (string.IsNullOrEmpty(resourceName)) + { + return null; + } + + if (_cachedResources == null) + { + DiscoverAllResources(); + } + + return _cachedResources.TryGetValue(resourceName, out var metadata) ? metadata : null; + } + + public List GetEnabledResources() + { + return DiscoverAllResources() + .Where(r => IsResourceEnabled(r.Name)) + .ToList(); + } + + public bool IsResourceEnabled(string resourceName) + { + if (string.IsNullOrEmpty(resourceName)) + { + return false; + } + + string key = GetResourcePreferenceKey(resourceName); + if (EditorPrefs.HasKey(key)) + { + return EditorPrefs.GetBool(key, true); + } + + // Default: all resources enabled + return true; + } + + public void SetResourceEnabled(string resourceName, bool enabled) + { + if (string.IsNullOrEmpty(resourceName)) + { + return; + } + + string key = GetResourcePreferenceKey(resourceName); + EditorPrefs.SetBool(key, enabled); + } + + public void InvalidateCache() + { + _cachedResources = null; + } + + private ResourceMetadata ExtractResourceMetadata(Type type, McpForUnityResourceAttribute resourceAttr) + { + try + { + string resourceName = resourceAttr.ResourceName; + if (string.IsNullOrEmpty(resourceName)) + { + resourceName = StringCaseUtility.ToSnakeCase(type.Name); + } + + string description = resourceAttr.Description ?? $"Resource: {resourceName}"; + + var metadata = new ResourceMetadata + { + Name = resourceName, + Description = description, + ClassName = type.Name, + Namespace = type.Namespace ?? "", + AssemblyName = type.Assembly.GetName().Name + }; + + metadata.IsBuiltIn = StringCaseUtility.IsBuiltInMcpType( + type, metadata.AssemblyName, "MCPForUnity.Editor.Resources"); + + return metadata; + } + catch (Exception ex) + { + McpLog.Error($"Failed to extract metadata for resource {type.Name}: {ex.Message}"); + return null; + } + } + + private void EnsurePreferenceInitialized(ResourceMetadata metadata) + { + if (metadata == null || string.IsNullOrEmpty(metadata.Name)) + { + return; + } + + string key = GetResourcePreferenceKey(metadata.Name); + if (!EditorPrefs.HasKey(key)) + { + EditorPrefs.SetBool(key, true); + } + } + + private static string GetResourcePreferenceKey(string resourceName) + { + return EditorPrefKeys.ResourceEnabledPrefix + resourceName; + } + } +} diff --git a/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs.meta b/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs.meta new file mode 100644 index 000000000..61a096cc5 --- /dev/null +++ b/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 66ce49d2cc47a4bd3aa85ac9f099b757 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/ToolDiscoveryService.cs b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs index 10578436d..b5b86c0a2 100644 --- a/MCPForUnity/Editor/Services/ToolDiscoveryService.cs +++ b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs @@ -45,6 +45,10 @@ public List DiscoverAllTools() var metadata = ExtractToolMetadata(type, toolAttr); if (metadata != null) { + if (_cachedTools.ContainsKey(metadata.Name)) + { + McpLog.Warn($"Duplicate tool name '{metadata.Name}' from {type.FullName}; overwriting previous registration."); + } _cachedTools[metadata.Name] = metadata; EnsurePreferenceInitialized(metadata); } @@ -131,7 +135,8 @@ private ToolMetadata ExtractToolMetadata(Type type, McpForUnityToolAttribute too PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction }; - metadata.IsBuiltIn = DetermineIsBuiltIn(type, metadata); + metadata.IsBuiltIn = StringCaseUtility.IsBuiltInMcpType( + type, metadata.AssemblyName, "MCPForUnity.Editor.Tools"); return metadata; @@ -239,24 +244,5 @@ private static string GetToolPreferenceKey(string toolName) return EditorPrefKeys.ToolEnabledPrefix + toolName; } - private bool DetermineIsBuiltIn(Type type, ToolMetadata metadata) - { - if (metadata == null) - { - return false; - } - - if (type != null && !string.IsNullOrEmpty(type.Namespace) && type.Namespace.StartsWith("MCPForUnity.Editor.Tools", StringComparison.Ordinal)) - { - return true; - } - - if (!string.IsNullOrEmpty(metadata.AssemblyName) && metadata.AssemblyName.Equals("MCPForUnity.Editor", StringComparison.Ordinal)) - { - return true; - } - - return false; - } } } diff --git a/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs b/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs index 999c7bf11..86f858d39 100644 --- a/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs +++ b/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; +using MCPForUnity.Editor.Services; using MCPForUnity.Editor.Tools; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -337,6 +338,27 @@ private static void ProcessCommand(string id, PendingCommand pending) } var parameters = command.@params ?? new JObject(); + + // Block execution of disabled resources + var resourceMeta = MCPServiceLocator.ResourceDiscovery.GetResourceMetadata(command.type); + if (resourceMeta != null && !MCPServiceLocator.ResourceDiscovery.IsResourceEnabled(command.type)) + { + pending.TrySetResult(SerializeError( + $"Resource '{command.type}' is disabled in the Unity Editor.")); + RemovePending(id, pending); + return; + } + + // Block execution of disabled tools + var toolMeta = MCPServiceLocator.ToolDiscovery.GetToolMetadata(command.type); + if (toolMeta != null && !MCPServiceLocator.ToolDiscovery.IsToolEnabled(command.type)) + { + pending.TrySetResult(SerializeError( + $"Tool '{command.type}' is disabled in the Unity Editor.")); + RemovePending(id, pending); + return; + } + var result = CommandRegistry.ExecuteCommand(command.type, parameters, pending.CompletionSource); if (result == null) diff --git a/MCPForUnity/Editor/Windows/Components/Resources.meta b/MCPForUnity/Editor/Windows/Components/Resources.meta new file mode 100644 index 000000000..35b89e23f --- /dev/null +++ b/MCPForUnity/Editor/Windows/Components/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 582ec97120b80401cb943b45d15425f9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Windows/Components/Resources/McpResourcesSection.cs b/MCPForUnity/Editor/Windows/Components/Resources/McpResourcesSection.cs new file mode 100644 index 000000000..2682d4a5b --- /dev/null +++ b/MCPForUnity/Editor/Windows/Components/Resources/McpResourcesSection.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MCPForUnity.Editor.Constants; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Services; +using UnityEditor; +using UnityEngine.UIElements; + +namespace MCPForUnity.Editor.Windows.Components.Resources +{ + /// + /// Controller for the Resources section inside the MCP For Unity editor window. + /// Provides discovery, filtering, and per-resource enablement toggles. + /// + public class McpResourcesSection + { + private readonly Dictionary resourceToggleMap = new(); + private Label summaryLabel; + private Label noteLabel; + private Button enableAllButton; + private Button disableAllButton; + private Button rescanButton; + private VisualElement categoryContainer; + private List allResources = new(); + + public VisualElement Root { get; } + + public McpResourcesSection(VisualElement root) + { + Root = root; + CacheUIElements(); + RegisterCallbacks(); + } + + private void CacheUIElements() + { + summaryLabel = Root.Q