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