Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
167 changes: 167 additions & 0 deletions MCPForUnity/Editor/Services/ResourceDiscoveryService.cs
Original file line number Diff line number Diff line change
@@ -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<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>();
}
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<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.

26 changes: 6 additions & 20 deletions MCPForUnity/Editor/Services/ToolDiscoveryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ public List<ToolMetadata> 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);
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
}
}
Loading