From a2a73dfa07e761df78f01e98b3b1cda1e2fe2593 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Fri, 30 Jan 2026 18:57:58 -0400 Subject: [PATCH 1/6] Add resource discovery service and UI for managing MCP resources --- .../Editor/Constants/EditorPrefKeys.cs | 2 + .../Resources/McpForUnityResourceAttribute.cs | 5 + .../Services/IResourceDiscoveryService.cs | 53 ++++ .../IResourceDiscoveryService.cs.meta | 11 + .../Editor/Services/MCPServiceLocator.cs | 6 + .../Services/ResourceDiscoveryService.cs | 177 +++++++++++++ .../Services/ResourceDiscoveryService.cs.meta | 11 + .../Editor/Windows/Components/Resources.meta | 8 + .../Resources/McpResourcesSection.cs | 250 ++++++++++++++++++ .../Resources/McpResourcesSection.cs.meta | 11 + .../Resources/McpResourcesSection.uxml | 15 ++ .../Resources/McpResourcesSection.uxml.meta | 10 + .../Editor/Windows/MCPForUnityEditorWindow.cs | 75 +++++- .../Windows/MCPForUnityEditorWindow.uxml | 4 + 14 files changed, 636 insertions(+), 2 deletions(-) create mode 100644 MCPForUnity/Editor/Services/IResourceDiscoveryService.cs create mode 100644 MCPForUnity/Editor/Services/IResourceDiscoveryService.cs.meta create mode 100644 MCPForUnity/Editor/Services/ResourceDiscoveryService.cs create mode 100644 MCPForUnity/Editor/Services/ResourceDiscoveryService.cs.meta create mode 100644 MCPForUnity/Editor/Windows/Components/Resources.meta create mode 100644 MCPForUnity/Editor/Windows/Components/Resources/McpResourcesSection.cs create mode 100644 MCPForUnity/Editor/Windows/Components/Resources/McpResourcesSection.cs.meta create mode 100644 MCPForUnity/Editor/Windows/Components/Resources/McpResourcesSection.uxml create mode 100644 MCPForUnity/Editor/Windows/Components/Resources/McpResourcesSection.uxml.meta 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/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..501cb92f6 --- /dev/null +++ b/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs @@ -0,0 +1,177 @@ +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) + { + _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 (_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 = DetermineIsBuiltIn(type, metadata); + + 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; + } + + private bool DetermineIsBuiltIn(Type type, ResourceMetadata metadata) + { + if (metadata == null) + { + return false; + } + + if (type != null && !string.IsNullOrEmpty(type.Namespace) && type.Namespace.StartsWith("MCPForUnity.Editor.Resources", 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/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/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