Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
23 changes: 23 additions & 0 deletions MCPForUnity/Editor/Helpers/StringCaseUtility.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;

Expand All @@ -10,6 +11,28 @@ namespace MCPForUnity.Editor.Helpers
/// </summary>
public static class StringCaseUtility
{
/// <summary>
/// Checks whether a type belongs to the built-in MCP for Unity package.
/// Returns true when the type's namespace starts with
/// <paramref name="builtInNamespacePrefix"/> or its assembly is MCPForUnity.Editor.
/// </summary>
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;
}

/// <summary>
/// Converts a camelCase string to snake_case.
/// Example: "searchMethod" -> "search_method", "param1Value" -> "param1_value"
Expand Down
5 changes: 5 additions & 0 deletions MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public class McpForUnityResourceAttribute : Attribute
/// </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).
Expand Down
53 changes: 53 additions & 0 deletions MCPForUnity/Editor/Services/IResourceDiscoveryService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Collections.Generic;

namespace MCPForUnity.Editor.Services
{
/// <summary>
/// Metadata for a discovered resource
/// </summary>
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; }
}

/// <summary>
/// Service for discovering MCP resources via reflection
/// </summary>
public interface IResourceDiscoveryService
{
/// <summary>
/// Discovers all resources marked with [McpForUnityResource]
/// </summary>
List<ResourceMetadata> DiscoverAllResources();

/// <summary>
/// Gets metadata for a specific resource
/// </summary>
ResourceMetadata GetResourceMetadata(string resourceName);

/// <summary>
/// Returns only the resources currently enabled
/// </summary>
List<ResourceMetadata> GetEnabledResources();

/// <summary>
/// Checks whether a resource is currently enabled
/// </summary>
bool IsResourceEnabled(string resourceName);

/// <summary>
/// Updates the enabled state for a resource
/// </summary>
void SetResourceEnabled(string resourceName, bool enabled);

/// <summary>
/// Invalidates the resource discovery cache
/// </summary>
void InvalidateCache();
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Services/IResourceDiscoveryService.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions MCPForUnity/Editor/Services/MCPServiceLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -53,6 +55,8 @@ public static void Register<T>(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)
Expand All @@ -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();
Expand All @@ -84,6 +89,7 @@ public static void Reset()
_packageUpdateService = null;
_platformService = null;
_toolDiscoveryService = null;
_resourceDiscoveryService = null;
_serverManagementService = null;
_transportManager = null;
_packageDeploymentService = null;
Expand Down
158 changes: 158 additions & 0 deletions MCPForUnity/Editor/Services/ResourceDiscoveryService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
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<string, ResourceMetadata> _cachedResources;

public List<ResourceMetadata> DiscoverAllResources()
{
if (_cachedResources != null)
{
return _cachedResources.Values.ToList();
}

_cachedResources = new Dictionary<string, ResourceMetadata>();

var resourceTypes = TypeCache.GetTypesWithAttribute<McpForUnityResourceAttribute>();
foreach (var type in resourceTypes)
{
McpForUnityResourceAttribute resourceAttr;
try
{
resourceAttr = type.GetCustomAttribute<McpForUnityResourceAttribute>();
}
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
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);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

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;
}
Comment thread
msanatan marked this conversation as resolved.

public List<ResourceMetadata> 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;
}
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Services/ResourceDiscoveryService.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 2 additions & 20 deletions MCPForUnity/Editor/Services/ToolDiscoveryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,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;

Expand Down Expand Up @@ -239,24 +240,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;
}
}
}
Loading