Skip to content

Commit 61284cc

Browse files
Display resources (#658)
* Add resource discovery service and UI for managing MCP resources * Consolidate duplicate IsBuiltIn logic into StringCaseUtility.IsBuiltInMcpType * Add resource enable/disable enforcement and improve error response handling - Block execution of disabled resources in TransportCommandDispatcher with clear error message - Add parse_resource_response() utility to handle error responses without Pydantic validation failures - Replace inline response parsing with parse_resource_response() across all resource handlers - Export parse_resource_response from models/__init__.py for consistent usage * Block execution of disabled built-in tools in TransportCommandDispatcher with clear error message Add tool enable/disable enforcement before command execution. Check tool metadata and enabled state, returning error response if tool is disabled. Prevents execution of disabled tools with user-friendly error message. * Fire warning in the rare chance there are duplicate names * Handle rare case a resource name is null Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 664a43b commit 61284cc

28 files changed

Lines changed: 722 additions & 35 deletions

‎MCPForUnity/Editor/Constants/EditorPrefKeys.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ internal static class EditorPrefKeys
4343
internal const string AutoRegisterEnabled = "MCPForUnity.AutoRegisterEnabled";
4444
internal const string ToolEnabledPrefix = "MCPForUnity.ToolEnabled.";
4545
internal const string ToolFoldoutStatePrefix = "MCPForUnity.ToolFoldout.";
46+
internal const string ResourceEnabledPrefix = "MCPForUnity.ResourceEnabled.";
47+
internal const string ResourceFoldoutStatePrefix = "MCPForUnity.ResourceFoldout.";
4648
internal const string EditorWindowActivePanel = "MCPForUnity.EditorWindow.ActivePanel";
4749

4850
internal const string SetupCompleted = "MCPForUnity.SetupCompleted";

‎MCPForUnity/Editor/Helpers/StringCaseUtility.cs‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Linq;
23
using System.Text.RegularExpressions;
34

@@ -10,6 +11,28 @@ namespace MCPForUnity.Editor.Helpers
1011
/// </summary>
1112
public static class StringCaseUtility
1213
{
14+
/// <summary>
15+
/// Checks whether a type belongs to the built-in MCP for Unity package.
16+
/// Returns true when the type's namespace starts with
17+
/// <paramref name="builtInNamespacePrefix"/> or its assembly is MCPForUnity.Editor.
18+
/// </summary>
19+
public static bool IsBuiltInMcpType(Type type, string assemblyName, string builtInNamespacePrefix)
20+
{
21+
if (type != null && !string.IsNullOrEmpty(type.Namespace)
22+
&& type.Namespace.StartsWith(builtInNamespacePrefix, StringComparison.Ordinal))
23+
{
24+
return true;
25+
}
26+
27+
if (!string.IsNullOrEmpty(assemblyName)
28+
&& assemblyName.Equals("MCPForUnity.Editor", StringComparison.Ordinal))
29+
{
30+
return true;
31+
}
32+
33+
return false;
34+
}
35+
1336
/// <summary>
1437
/// Converts a camelCase string to snake_case.
1538
/// Example: "searchMethod" -> "search_method", "param1Value" -> "param1_value"

‎MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ public class McpForUnityResourceAttribute : Attribute
1515
/// </summary>
1616
public string ResourceName { get; }
1717

18+
/// <summary>
19+
/// Human-readable description of what this resource provides.
20+
/// </summary>
21+
public string Description { get; set; }
22+
1823
/// <summary>
1924
/// Create an MCP resource attribute with auto-generated resource name.
2025
/// The resource name will be derived from the class name (PascalCase → snake_case).
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Collections.Generic;
2+
3+
namespace MCPForUnity.Editor.Services
4+
{
5+
/// <summary>
6+
/// Metadata for a discovered resource
7+
/// </summary>
8+
public class ResourceMetadata
9+
{
10+
public string Name { get; set; }
11+
public string Description { get; set; }
12+
public string ClassName { get; set; }
13+
public string Namespace { get; set; }
14+
public string AssemblyName { get; set; }
15+
public bool IsBuiltIn { get; set; }
16+
}
17+
18+
/// <summary>
19+
/// Service for discovering MCP resources via reflection
20+
/// </summary>
21+
public interface IResourceDiscoveryService
22+
{
23+
/// <summary>
24+
/// Discovers all resources marked with [McpForUnityResource]
25+
/// </summary>
26+
List<ResourceMetadata> DiscoverAllResources();
27+
28+
/// <summary>
29+
/// Gets metadata for a specific resource
30+
/// </summary>
31+
ResourceMetadata GetResourceMetadata(string resourceName);
32+
33+
/// <summary>
34+
/// Returns only the resources currently enabled
35+
/// </summary>
36+
List<ResourceMetadata> GetEnabledResources();
37+
38+
/// <summary>
39+
/// Checks whether a resource is currently enabled
40+
/// </summary>
41+
bool IsResourceEnabled(string resourceName);
42+
43+
/// <summary>
44+
/// Updates the enabled state for a resource
45+
/// </summary>
46+
void SetResourceEnabled(string resourceName, bool enabled);
47+
48+
/// <summary>
49+
/// Invalidates the resource discovery cache
50+
/// </summary>
51+
void InvalidateCache();
52+
}
53+
}

‎MCPForUnity/Editor/Services/IResourceDiscoveryService.cs.meta‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎MCPForUnity/Editor/Services/MCPServiceLocator.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public static class MCPServiceLocator
1717
private static IPackageUpdateService _packageUpdateService;
1818
private static IPlatformService _platformService;
1919
private static IToolDiscoveryService _toolDiscoveryService;
20+
private static IResourceDiscoveryService _resourceDiscoveryService;
2021
private static IServerManagementService _serverManagementService;
2122
private static TransportManager _transportManager;
2223
private static IPackageDeploymentService _packageDeploymentService;
@@ -28,6 +29,7 @@ public static class MCPServiceLocator
2829
public static IPackageUpdateService Updates => _packageUpdateService ??= new PackageUpdateService();
2930
public static IPlatformService Platform => _platformService ??= new PlatformService();
3031
public static IToolDiscoveryService ToolDiscovery => _toolDiscoveryService ??= new ToolDiscoveryService();
32+
public static IResourceDiscoveryService ResourceDiscovery => _resourceDiscoveryService ??= new ResourceDiscoveryService();
3133
public static IServerManagementService Server => _serverManagementService ??= new ServerManagementService();
3234
public static TransportManager TransportManager => _transportManager ??= new TransportManager();
3335
public static IPackageDeploymentService Deployment => _packageDeploymentService ??= new PackageDeploymentService();
@@ -53,6 +55,8 @@ public static void Register<T>(T implementation) where T : class
5355
_platformService = ps;
5456
else if (implementation is IToolDiscoveryService td)
5557
_toolDiscoveryService = td;
58+
else if (implementation is IResourceDiscoveryService rd)
59+
_resourceDiscoveryService = rd;
5660
else if (implementation is IServerManagementService sm)
5761
_serverManagementService = sm;
5862
else if (implementation is IPackageDeploymentService pd)
@@ -73,6 +77,7 @@ public static void Reset()
7377
(_packageUpdateService as IDisposable)?.Dispose();
7478
(_platformService as IDisposable)?.Dispose();
7579
(_toolDiscoveryService as IDisposable)?.Dispose();
80+
(_resourceDiscoveryService as IDisposable)?.Dispose();
7681
(_serverManagementService as IDisposable)?.Dispose();
7782
(_transportManager as IDisposable)?.Dispose();
7883
(_packageDeploymentService as IDisposable)?.Dispose();
@@ -84,6 +89,7 @@ public static void Reset()
8489
_packageUpdateService = null;
8590
_platformService = null;
8691
_toolDiscoveryService = null;
92+
_resourceDiscoveryService = null;
8793
_serverManagementService = null;
8894
_transportManager = null;
8995
_packageDeploymentService = null;
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using MCPForUnity.Editor.Constants;
6+
using MCPForUnity.Editor.Helpers;
7+
using MCPForUnity.Editor.Resources;
8+
using UnityEditor;
9+
10+
namespace MCPForUnity.Editor.Services
11+
{
12+
public class ResourceDiscoveryService : IResourceDiscoveryService
13+
{
14+
private Dictionary<string, ResourceMetadata> _cachedResources;
15+
16+
public List<ResourceMetadata> DiscoverAllResources()
17+
{
18+
if (_cachedResources != null)
19+
{
20+
return _cachedResources.Values.ToList();
21+
}
22+
23+
_cachedResources = new Dictionary<string, ResourceMetadata>();
24+
25+
var resourceTypes = TypeCache.GetTypesWithAttribute<McpForUnityResourceAttribute>();
26+
foreach (var type in resourceTypes)
27+
{
28+
McpForUnityResourceAttribute resourceAttr;
29+
try
30+
{
31+
resourceAttr = type.GetCustomAttribute<McpForUnityResourceAttribute>();
32+
}
33+
catch (Exception ex)
34+
{
35+
McpLog.Warn($"Failed to read [McpForUnityResource] for {type.FullName}: {ex.Message}");
36+
continue;
37+
}
38+
39+
if (resourceAttr == null)
40+
{
41+
continue;
42+
}
43+
44+
var metadata = ExtractResourceMetadata(type, resourceAttr);
45+
if (metadata != null)
46+
{
47+
if (_cachedResources.ContainsKey(metadata.Name))
48+
{
49+
McpLog.Warn($"Duplicate resource name '{metadata.Name}' from {type.FullName}; overwriting previous registration.");
50+
}
51+
_cachedResources[metadata.Name] = metadata;
52+
EnsurePreferenceInitialized(metadata);
53+
}
54+
}
55+
56+
McpLog.Info($"Discovered {_cachedResources.Count} MCP resources via reflection", false);
57+
return _cachedResources.Values.ToList();
58+
}
59+
60+
public ResourceMetadata GetResourceMetadata(string resourceName)
61+
{
62+
if (string.IsNullOrEmpty(resourceName))
63+
{
64+
return null;
65+
}
66+
67+
if (_cachedResources == null)
68+
{
69+
DiscoverAllResources();
70+
}
71+
72+
return _cachedResources.TryGetValue(resourceName, out var metadata) ? metadata : null;
73+
}
74+
75+
public List<ResourceMetadata> GetEnabledResources()
76+
{
77+
return DiscoverAllResources()
78+
.Where(r => IsResourceEnabled(r.Name))
79+
.ToList();
80+
}
81+
82+
public bool IsResourceEnabled(string resourceName)
83+
{
84+
if (string.IsNullOrEmpty(resourceName))
85+
{
86+
return false;
87+
}
88+
89+
string key = GetResourcePreferenceKey(resourceName);
90+
if (EditorPrefs.HasKey(key))
91+
{
92+
return EditorPrefs.GetBool(key, true);
93+
}
94+
95+
// Default: all resources enabled
96+
return true;
97+
}
98+
99+
public void SetResourceEnabled(string resourceName, bool enabled)
100+
{
101+
if (string.IsNullOrEmpty(resourceName))
102+
{
103+
return;
104+
}
105+
106+
string key = GetResourcePreferenceKey(resourceName);
107+
EditorPrefs.SetBool(key, enabled);
108+
}
109+
110+
public void InvalidateCache()
111+
{
112+
_cachedResources = null;
113+
}
114+
115+
private ResourceMetadata ExtractResourceMetadata(Type type, McpForUnityResourceAttribute resourceAttr)
116+
{
117+
try
118+
{
119+
string resourceName = resourceAttr.ResourceName;
120+
if (string.IsNullOrEmpty(resourceName))
121+
{
122+
resourceName = StringCaseUtility.ToSnakeCase(type.Name);
123+
}
124+
125+
string description = resourceAttr.Description ?? $"Resource: {resourceName}";
126+
127+
var metadata = new ResourceMetadata
128+
{
129+
Name = resourceName,
130+
Description = description,
131+
ClassName = type.Name,
132+
Namespace = type.Namespace ?? "",
133+
AssemblyName = type.Assembly.GetName().Name
134+
};
135+
136+
metadata.IsBuiltIn = StringCaseUtility.IsBuiltInMcpType(
137+
type, metadata.AssemblyName, "MCPForUnity.Editor.Resources");
138+
139+
return metadata;
140+
}
141+
catch (Exception ex)
142+
{
143+
McpLog.Error($"Failed to extract metadata for resource {type.Name}: {ex.Message}");
144+
return null;
145+
}
146+
}
147+
148+
private void EnsurePreferenceInitialized(ResourceMetadata metadata)
149+
{
150+
if (metadata == null || string.IsNullOrEmpty(metadata.Name))
151+
{
152+
return;
153+
}
154+
155+
string key = GetResourcePreferenceKey(metadata.Name);
156+
if (!EditorPrefs.HasKey(key))
157+
{
158+
EditorPrefs.SetBool(key, true);
159+
}
160+
}
161+
162+
private static string GetResourcePreferenceKey(string resourceName)
163+
{
164+
return EditorPrefKeys.ResourceEnabledPrefix + resourceName;
165+
}
166+
}
167+
}

‎MCPForUnity/Editor/Services/ResourceDiscoveryService.cs.meta‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎MCPForUnity/Editor/Services/ToolDiscoveryService.cs‎

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public List<ToolMetadata> DiscoverAllTools()
4545
var metadata = ExtractToolMetadata(type, toolAttr);
4646
if (metadata != null)
4747
{
48+
if (_cachedTools.ContainsKey(metadata.Name))
49+
{
50+
McpLog.Warn($"Duplicate tool name '{metadata.Name}' from {type.FullName}; overwriting previous registration.");
51+
}
4852
_cachedTools[metadata.Name] = metadata;
4953
EnsurePreferenceInitialized(metadata);
5054
}
@@ -131,7 +135,8 @@ private ToolMetadata ExtractToolMetadata(Type type, McpForUnityToolAttribute too
131135
PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction
132136
};
133137

134-
metadata.IsBuiltIn = DetermineIsBuiltIn(type, metadata);
138+
metadata.IsBuiltIn = StringCaseUtility.IsBuiltInMcpType(
139+
type, metadata.AssemblyName, "MCPForUnity.Editor.Tools");
135140

136141
return metadata;
137142

@@ -239,24 +244,5 @@ private static string GetToolPreferenceKey(string toolName)
239244
return EditorPrefKeys.ToolEnabledPrefix + toolName;
240245
}
241246

242-
private bool DetermineIsBuiltIn(Type type, ToolMetadata metadata)
243-
{
244-
if (metadata == null)
245-
{
246-
return false;
247-
}
248-
249-
if (type != null && !string.IsNullOrEmpty(type.Namespace) && type.Namespace.StartsWith("MCPForUnity.Editor.Tools", StringComparison.Ordinal))
250-
{
251-
return true;
252-
}
253-
254-
if (!string.IsNullOrEmpty(metadata.AssemblyName) && metadata.AssemblyName.Equals("MCPForUnity.Editor", StringComparison.Ordinal))
255-
{
256-
return true;
257-
}
258-
259-
return false;
260-
}
261247
}
262248
}

0 commit comments

Comments
 (0)