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>main
parent
664a43b76c
commit
61284cc172
|
|
@ -43,6 +43,8 @@ namespace MCPForUnity.Editor.Constants
|
||||||
internal const string AutoRegisterEnabled = "MCPForUnity.AutoRegisterEnabled";
|
internal const string AutoRegisterEnabled = "MCPForUnity.AutoRegisterEnabled";
|
||||||
internal const string ToolEnabledPrefix = "MCPForUnity.ToolEnabled.";
|
internal const string ToolEnabledPrefix = "MCPForUnity.ToolEnabled.";
|
||||||
internal const string ToolFoldoutStatePrefix = "MCPForUnity.ToolFoldout.";
|
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 EditorWindowActivePanel = "MCPForUnity.EditorWindow.ActivePanel";
|
||||||
|
|
||||||
internal const string SetupCompleted = "MCPForUnity.SetupCompleted";
|
internal const string SetupCompleted = "MCPForUnity.SetupCompleted";
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
|
@ -10,6 +11,28 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class StringCaseUtility
|
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>
|
/// <summary>
|
||||||
/// Converts a camelCase string to snake_case.
|
/// Converts a camelCase string to snake_case.
|
||||||
/// Example: "searchMethod" -> "search_method", "param1Value" -> "param1_value"
|
/// Example: "searchMethod" -> "search_method", "param1Value" -> "param1_value"
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ namespace MCPForUnity.Editor.Resources
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ResourceName { get; }
|
public string ResourceName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Human-readable description of what this resource provides.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create an MCP resource attribute with auto-generated resource name.
|
/// Create an MCP resource attribute with auto-generated resource name.
|
||||||
/// The resource name will be derived from the class name (PascalCase → snake_case).
|
/// The resource name will be derived from the class name (PascalCase → snake_case).
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7afb4739669224c74b4b4d706e6bbb49
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -17,6 +17,7 @@ namespace MCPForUnity.Editor.Services
|
||||||
private static IPackageUpdateService _packageUpdateService;
|
private static IPackageUpdateService _packageUpdateService;
|
||||||
private static IPlatformService _platformService;
|
private static IPlatformService _platformService;
|
||||||
private static IToolDiscoveryService _toolDiscoveryService;
|
private static IToolDiscoveryService _toolDiscoveryService;
|
||||||
|
private static IResourceDiscoveryService _resourceDiscoveryService;
|
||||||
private static IServerManagementService _serverManagementService;
|
private static IServerManagementService _serverManagementService;
|
||||||
private static TransportManager _transportManager;
|
private static TransportManager _transportManager;
|
||||||
private static IPackageDeploymentService _packageDeploymentService;
|
private static IPackageDeploymentService _packageDeploymentService;
|
||||||
|
|
@ -28,6 +29,7 @@ namespace MCPForUnity.Editor.Services
|
||||||
public static IPackageUpdateService Updates => _packageUpdateService ??= new PackageUpdateService();
|
public static IPackageUpdateService Updates => _packageUpdateService ??= new PackageUpdateService();
|
||||||
public static IPlatformService Platform => _platformService ??= new PlatformService();
|
public static IPlatformService Platform => _platformService ??= new PlatformService();
|
||||||
public static IToolDiscoveryService ToolDiscovery => _toolDiscoveryService ??= new ToolDiscoveryService();
|
public static IToolDiscoveryService ToolDiscovery => _toolDiscoveryService ??= new ToolDiscoveryService();
|
||||||
|
public static IResourceDiscoveryService ResourceDiscovery => _resourceDiscoveryService ??= new ResourceDiscoveryService();
|
||||||
public static IServerManagementService Server => _serverManagementService ??= new ServerManagementService();
|
public static IServerManagementService Server => _serverManagementService ??= new ServerManagementService();
|
||||||
public static TransportManager TransportManager => _transportManager ??= new TransportManager();
|
public static TransportManager TransportManager => _transportManager ??= new TransportManager();
|
||||||
public static IPackageDeploymentService Deployment => _packageDeploymentService ??= new PackageDeploymentService();
|
public static IPackageDeploymentService Deployment => _packageDeploymentService ??= new PackageDeploymentService();
|
||||||
|
|
@ -53,6 +55,8 @@ namespace MCPForUnity.Editor.Services
|
||||||
_platformService = ps;
|
_platformService = ps;
|
||||||
else if (implementation is IToolDiscoveryService td)
|
else if (implementation is IToolDiscoveryService td)
|
||||||
_toolDiscoveryService = td;
|
_toolDiscoveryService = td;
|
||||||
|
else if (implementation is IResourceDiscoveryService rd)
|
||||||
|
_resourceDiscoveryService = rd;
|
||||||
else if (implementation is IServerManagementService sm)
|
else if (implementation is IServerManagementService sm)
|
||||||
_serverManagementService = sm;
|
_serverManagementService = sm;
|
||||||
else if (implementation is IPackageDeploymentService pd)
|
else if (implementation is IPackageDeploymentService pd)
|
||||||
|
|
@ -73,6 +77,7 @@ namespace MCPForUnity.Editor.Services
|
||||||
(_packageUpdateService as IDisposable)?.Dispose();
|
(_packageUpdateService as IDisposable)?.Dispose();
|
||||||
(_platformService as IDisposable)?.Dispose();
|
(_platformService as IDisposable)?.Dispose();
|
||||||
(_toolDiscoveryService as IDisposable)?.Dispose();
|
(_toolDiscoveryService as IDisposable)?.Dispose();
|
||||||
|
(_resourceDiscoveryService as IDisposable)?.Dispose();
|
||||||
(_serverManagementService as IDisposable)?.Dispose();
|
(_serverManagementService as IDisposable)?.Dispose();
|
||||||
(_transportManager as IDisposable)?.Dispose();
|
(_transportManager as IDisposable)?.Dispose();
|
||||||
(_packageDeploymentService as IDisposable)?.Dispose();
|
(_packageDeploymentService as IDisposable)?.Dispose();
|
||||||
|
|
@ -84,6 +89,7 @@ namespace MCPForUnity.Editor.Services
|
||||||
_packageUpdateService = null;
|
_packageUpdateService = null;
|
||||||
_platformService = null;
|
_platformService = null;
|
||||||
_toolDiscoveryService = null;
|
_toolDiscoveryService = null;
|
||||||
|
_resourceDiscoveryService = null;
|
||||||
_serverManagementService = null;
|
_serverManagementService = null;
|
||||||
_transportManager = null;
|
_transportManager = null;
|
||||||
_packageDeploymentService = null;
|
_packageDeploymentService = null;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 66ce49d2cc47a4bd3aa85ac9f099b757
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -45,6 +45,10 @@ namespace MCPForUnity.Editor.Services
|
||||||
var metadata = ExtractToolMetadata(type, toolAttr);
|
var metadata = ExtractToolMetadata(type, toolAttr);
|
||||||
if (metadata != null)
|
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;
|
_cachedTools[metadata.Name] = metadata;
|
||||||
EnsurePreferenceInitialized(metadata);
|
EnsurePreferenceInitialized(metadata);
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +135,8 @@ namespace MCPForUnity.Editor.Services
|
||||||
PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction
|
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;
|
return metadata;
|
||||||
|
|
||||||
|
|
@ -239,24 +244,5 @@ namespace MCPForUnity.Editor.Services
|
||||||
return EditorPrefKeys.ToolEnabledPrefix + 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MCPForUnity.Editor.Helpers;
|
using MCPForUnity.Editor.Helpers;
|
||||||
using MCPForUnity.Editor.Models;
|
using MCPForUnity.Editor.Models;
|
||||||
|
using MCPForUnity.Editor.Services;
|
||||||
using MCPForUnity.Editor.Tools;
|
using MCPForUnity.Editor.Tools;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
@ -337,6 +338,27 @@ namespace MCPForUnity.Editor.Services.Transport
|
||||||
}
|
}
|
||||||
|
|
||||||
var parameters = command.@params ?? new JObject();
|
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);
|
var result = CommandRegistry.ExecuteCommand(command.type, parameters, pending.CompletionSource);
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 582ec97120b80401cb943b45d15425f9
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Controller for the Resources section inside the MCP For Unity editor window.
|
||||||
|
/// Provides discovery, filtering, and per-resource enablement toggles.
|
||||||
|
/// </summary>
|
||||||
|
public class McpResourcesSection
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, Toggle> resourceToggleMap = new();
|
||||||
|
private Label summaryLabel;
|
||||||
|
private Label noteLabel;
|
||||||
|
private Button enableAllButton;
|
||||||
|
private Button disableAllButton;
|
||||||
|
private Button rescanButton;
|
||||||
|
private VisualElement categoryContainer;
|
||||||
|
private List<ResourceMetadata> allResources = new();
|
||||||
|
|
||||||
|
public VisualElement Root { get; }
|
||||||
|
|
||||||
|
public McpResourcesSection(VisualElement root)
|
||||||
|
{
|
||||||
|
Root = root;
|
||||||
|
CacheUIElements();
|
||||||
|
RegisterCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CacheUIElements()
|
||||||
|
{
|
||||||
|
summaryLabel = Root.Q<Label>("resources-summary");
|
||||||
|
noteLabel = Root.Q<Label>("resources-note");
|
||||||
|
enableAllButton = Root.Q<Button>("enable-all-resources-button");
|
||||||
|
disableAllButton = Root.Q<Button>("disable-all-resources-button");
|
||||||
|
rescanButton = Root.Q<Button>("rescan-resources-button");
|
||||||
|
categoryContainer = Root.Q<VisualElement>("resource-category-container");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterCallbacks()
|
||||||
|
{
|
||||||
|
if (enableAllButton != null)
|
||||||
|
{
|
||||||
|
enableAllButton.AddToClassList("tool-action-button");
|
||||||
|
enableAllButton.style.marginRight = 4;
|
||||||
|
enableAllButton.clicked += () => SetAllResourcesState(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableAllButton != null)
|
||||||
|
{
|
||||||
|
disableAllButton.AddToClassList("tool-action-button");
|
||||||
|
disableAllButton.style.marginRight = 4;
|
||||||
|
disableAllButton.clicked += () => SetAllResourcesState(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rescanButton != null)
|
||||||
|
{
|
||||||
|
rescanButton.AddToClassList("tool-action-button");
|
||||||
|
rescanButton.clicked += () =>
|
||||||
|
{
|
||||||
|
McpLog.Info("Rescanning MCP resources from the editor window.");
|
||||||
|
MCPServiceLocator.ResourceDiscovery.InvalidateCache();
|
||||||
|
Refresh();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rebuilds the resource list and synchronises toggle states.
|
||||||
|
/// </summary>
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
resourceToggleMap.Clear();
|
||||||
|
categoryContainer?.Clear();
|
||||||
|
|
||||||
|
var service = MCPServiceLocator.ResourceDiscovery;
|
||||||
|
allResources = service.DiscoverAllResources()
|
||||||
|
.OrderBy(r => r.IsBuiltIn ? 0 : 1)
|
||||||
|
.ThenBy(r => r.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
bool hasResources = allResources.Count > 0;
|
||||||
|
enableAllButton?.SetEnabled(hasResources);
|
||||||
|
disableAllButton?.SetEnabled(hasResources);
|
||||||
|
|
||||||
|
if (noteLabel != null)
|
||||||
|
{
|
||||||
|
noteLabel.style.display = hasResources ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasResources)
|
||||||
|
{
|
||||||
|
AddInfoLabel("No MCP resources found. Add classes decorated with [McpForUnityResource] to expose resources.");
|
||||||
|
UpdateSummary();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildCategory("Built-in Resources", "built-in", allResources.Where(r => r.IsBuiltIn));
|
||||||
|
|
||||||
|
var customResources = allResources.Where(r => !r.IsBuiltIn).ToList();
|
||||||
|
if (customResources.Count > 0)
|
||||||
|
{
|
||||||
|
BuildCategory("Custom Resources", "custom", customResources);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddInfoLabel("No custom resources detected in loaded assemblies.");
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildCategory(string title, string prefsSuffix, IEnumerable<ResourceMetadata> resources)
|
||||||
|
{
|
||||||
|
var resourceList = resources.ToList();
|
||||||
|
if (resourceList.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var foldout = new Foldout
|
||||||
|
{
|
||||||
|
text = $"{title} ({resourceList.Count})",
|
||||||
|
value = EditorPrefs.GetBool(EditorPrefKeys.ResourceFoldoutStatePrefix + prefsSuffix, true)
|
||||||
|
};
|
||||||
|
|
||||||
|
foldout.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
EditorPrefs.SetBool(EditorPrefKeys.ResourceFoldoutStatePrefix + prefsSuffix, evt.newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var resource in resourceList)
|
||||||
|
{
|
||||||
|
foldout.Add(CreateResourceRow(resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryContainer?.Add(foldout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VisualElement CreateResourceRow(ResourceMetadata resource)
|
||||||
|
{
|
||||||
|
var row = new VisualElement();
|
||||||
|
row.AddToClassList("tool-item");
|
||||||
|
|
||||||
|
var header = new VisualElement();
|
||||||
|
header.AddToClassList("tool-item-header");
|
||||||
|
|
||||||
|
var toggle = new Toggle(resource.Name)
|
||||||
|
{
|
||||||
|
value = MCPServiceLocator.ResourceDiscovery.IsResourceEnabled(resource.Name)
|
||||||
|
};
|
||||||
|
toggle.AddToClassList("tool-item-toggle");
|
||||||
|
toggle.tooltip = string.IsNullOrWhiteSpace(resource.Description) ? resource.Name : resource.Description;
|
||||||
|
|
||||||
|
toggle.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
HandleToggleChange(resource, evt.newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
resourceToggleMap[resource.Name] = toggle;
|
||||||
|
header.Add(toggle);
|
||||||
|
|
||||||
|
var tagsContainer = new VisualElement();
|
||||||
|
tagsContainer.AddToClassList("tool-tags");
|
||||||
|
|
||||||
|
tagsContainer.Add(CreateTag(resource.IsBuiltIn ? "Built-in" : "Custom"));
|
||||||
|
|
||||||
|
header.Add(tagsContainer);
|
||||||
|
row.Add(header);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(resource.Description) && !resource.Description.StartsWith("Resource:", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var description = new Label(resource.Description);
|
||||||
|
description.AddToClassList("tool-item-description");
|
||||||
|
row.Add(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleToggleChange(ResourceMetadata resource, bool enabled, bool updateSummary = true)
|
||||||
|
{
|
||||||
|
MCPServiceLocator.ResourceDiscovery.SetResourceEnabled(resource.Name, enabled);
|
||||||
|
|
||||||
|
if (updateSummary)
|
||||||
|
{
|
||||||
|
UpdateSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetAllResourcesState(bool enabled)
|
||||||
|
{
|
||||||
|
foreach (var resource in allResources)
|
||||||
|
{
|
||||||
|
if (!resourceToggleMap.TryGetValue(resource.Name, out var toggle))
|
||||||
|
{
|
||||||
|
MCPServiceLocator.ResourceDiscovery.SetResourceEnabled(resource.Name, enabled);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toggle.value == enabled)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.SetValueWithoutNotify(enabled);
|
||||||
|
HandleToggleChange(resource, enabled, updateSummary: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSummary()
|
||||||
|
{
|
||||||
|
if (summaryLabel == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allResources.Count == 0)
|
||||||
|
{
|
||||||
|
summaryLabel.text = "No MCP resources discovered.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int enabledCount = allResources.Count(r => MCPServiceLocator.ResourceDiscovery.IsResourceEnabled(r.Name));
|
||||||
|
summaryLabel.text = $"{enabledCount} of {allResources.Count} resources enabled.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddInfoLabel(string message)
|
||||||
|
{
|
||||||
|
var label = new Label(message);
|
||||||
|
label.AddToClassList("help-text");
|
||||||
|
categoryContainer?.Add(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Label CreateTag(string text)
|
||||||
|
{
|
||||||
|
var tag = new Label(text);
|
||||||
|
tag.AddToClassList("tool-tag");
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 465cdffd91f9d461caf4298ca322e3ab
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
|
||||||
|
<ui:VisualElement name="resources-section" class="section">
|
||||||
|
<ui:Label text="Resources" class="section-title" />
|
||||||
|
<ui:VisualElement class="section-content">
|
||||||
|
<ui:Label name="resources-summary" class="help-text" text="Discovering resources..." />
|
||||||
|
<ui:VisualElement name="resources-actions" class="tool-actions">
|
||||||
|
<ui:Button name="enable-all-resources-button" text="Enable All" class="tool-action-button" />
|
||||||
|
<ui:Button name="disable-all-resources-button" text="Disable All" class="tool-action-button" />
|
||||||
|
<ui:Button name="rescan-resources-button" text="Rescan" class="tool-action-button" />
|
||||||
|
</ui:VisualElement>
|
||||||
|
<ui:Label name="resources-note" class="help-text" text="Changes apply after reconnecting or re-registering resources." />
|
||||||
|
<ui:VisualElement name="resource-category-container" class="tool-category-container" />
|
||||||
|
</ui:VisualElement>
|
||||||
|
</ui:VisualElement>
|
||||||
|
</ui:UXML>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b455fadaaad0a43c4bae9f3fe784c5c3
|
||||||
|
ScriptedImporter:
|
||||||
|
internalIDToNameTable: []
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
|
@ -8,6 +8,7 @@ using MCPForUnity.Editor.Services;
|
||||||
using MCPForUnity.Editor.Windows.Components.Advanced;
|
using MCPForUnity.Editor.Windows.Components.Advanced;
|
||||||
using MCPForUnity.Editor.Windows.Components.ClientConfig;
|
using MCPForUnity.Editor.Windows.Components.ClientConfig;
|
||||||
using MCPForUnity.Editor.Windows.Components.Connection;
|
using MCPForUnity.Editor.Windows.Components.Connection;
|
||||||
|
using MCPForUnity.Editor.Windows.Components.Resources;
|
||||||
using MCPForUnity.Editor.Windows.Components.Tools;
|
using MCPForUnity.Editor.Windows.Components.Tools;
|
||||||
using MCPForUnity.Editor.Windows.Components.Validation;
|
using MCPForUnity.Editor.Windows.Components.Validation;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
|
|
@ -25,6 +26,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
private McpValidationSection validationSection;
|
private McpValidationSection validationSection;
|
||||||
private McpAdvancedSection advancedSection;
|
private McpAdvancedSection advancedSection;
|
||||||
private McpToolsSection toolsSection;
|
private McpToolsSection toolsSection;
|
||||||
|
private McpResourcesSection resourcesSection;
|
||||||
|
|
||||||
// UI Elements
|
// UI Elements
|
||||||
private Label versionLabel;
|
private Label versionLabel;
|
||||||
|
|
@ -35,14 +37,17 @@ namespace MCPForUnity.Editor.Windows
|
||||||
private ToolbarToggle validationTabToggle;
|
private ToolbarToggle validationTabToggle;
|
||||||
private ToolbarToggle advancedTabToggle;
|
private ToolbarToggle advancedTabToggle;
|
||||||
private ToolbarToggle toolsTabToggle;
|
private ToolbarToggle toolsTabToggle;
|
||||||
|
private ToolbarToggle resourcesTabToggle;
|
||||||
private VisualElement clientsPanel;
|
private VisualElement clientsPanel;
|
||||||
private VisualElement validationPanel;
|
private VisualElement validationPanel;
|
||||||
private VisualElement advancedPanel;
|
private VisualElement advancedPanel;
|
||||||
private VisualElement toolsPanel;
|
private VisualElement toolsPanel;
|
||||||
|
private VisualElement resourcesPanel;
|
||||||
|
|
||||||
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
|
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
|
||||||
private bool guiCreated = false;
|
private bool guiCreated = false;
|
||||||
private bool toolsLoaded = false;
|
private bool toolsLoaded = false;
|
||||||
|
private bool resourcesLoaded = false;
|
||||||
private double lastRefreshTime = 0;
|
private double lastRefreshTime = 0;
|
||||||
private const double RefreshDebounceSeconds = 0.5;
|
private const double RefreshDebounceSeconds = 0.5;
|
||||||
|
|
||||||
|
|
@ -51,7 +56,8 @@ namespace MCPForUnity.Editor.Windows
|
||||||
Clients,
|
Clients,
|
||||||
Validation,
|
Validation,
|
||||||
Advanced,
|
Advanced,
|
||||||
Tools
|
Tools,
|
||||||
|
Resources
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void CloseAllWindows()
|
internal static void CloseAllWindows()
|
||||||
|
|
@ -146,12 +152,14 @@ namespace MCPForUnity.Editor.Windows
|
||||||
validationPanel = rootVisualElement.Q<VisualElement>("validation-panel");
|
validationPanel = rootVisualElement.Q<VisualElement>("validation-panel");
|
||||||
advancedPanel = rootVisualElement.Q<VisualElement>("advanced-panel");
|
advancedPanel = rootVisualElement.Q<VisualElement>("advanced-panel");
|
||||||
toolsPanel = rootVisualElement.Q<VisualElement>("tools-panel");
|
toolsPanel = rootVisualElement.Q<VisualElement>("tools-panel");
|
||||||
|
resourcesPanel = rootVisualElement.Q<VisualElement>("resources-panel");
|
||||||
var clientsContainer = rootVisualElement.Q<VisualElement>("clients-container");
|
var clientsContainer = rootVisualElement.Q<VisualElement>("clients-container");
|
||||||
var validationContainer = rootVisualElement.Q<VisualElement>("validation-container");
|
var validationContainer = rootVisualElement.Q<VisualElement>("validation-container");
|
||||||
var advancedContainer = rootVisualElement.Q<VisualElement>("advanced-container");
|
var advancedContainer = rootVisualElement.Q<VisualElement>("advanced-container");
|
||||||
var toolsContainer = rootVisualElement.Q<VisualElement>("tools-container");
|
var toolsContainer = rootVisualElement.Q<VisualElement>("tools-container");
|
||||||
|
var resourcesContainer = rootVisualElement.Q<VisualElement>("resources-container");
|
||||||
|
|
||||||
if (clientsPanel == null || validationPanel == null || advancedPanel == null || toolsPanel == null)
|
if (clientsPanel == null || validationPanel == null || advancedPanel == null || toolsPanel == null || resourcesPanel == null)
|
||||||
{
|
{
|
||||||
McpLog.Error("Failed to find tab panels in UXML");
|
McpLog.Error("Failed to find tab panels in UXML");
|
||||||
return;
|
return;
|
||||||
|
|
@ -181,6 +189,12 @@ namespace MCPForUnity.Editor.Windows
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resourcesContainer == null)
|
||||||
|
{
|
||||||
|
McpLog.Error("Failed to find resources-container in UXML");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize version label
|
// Initialize version label
|
||||||
UpdateVersionLabel(EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true));
|
UpdateVersionLabel(EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true));
|
||||||
|
|
||||||
|
|
@ -275,6 +289,26 @@ namespace MCPForUnity.Editor.Windows
|
||||||
McpLog.Warn("Failed to load tools section UXML. Tool configuration will be unavailable.");
|
McpLog.Warn("Failed to load tools section UXML. Tool configuration will be unavailable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load and initialize Resources section
|
||||||
|
var resourcesTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
|
||||||
|
$"{basePath}/Editor/Windows/Components/Resources/McpResourcesSection.uxml"
|
||||||
|
);
|
||||||
|
if (resourcesTree != null)
|
||||||
|
{
|
||||||
|
var resourcesRoot = resourcesTree.Instantiate();
|
||||||
|
resourcesContainer.Add(resourcesRoot);
|
||||||
|
resourcesSection = new McpResourcesSection(resourcesRoot);
|
||||||
|
|
||||||
|
if (resourcesTabToggle != null && resourcesTabToggle.value)
|
||||||
|
{
|
||||||
|
EnsureResourcesLoaded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
McpLog.Warn("Failed to load resources section UXML. Resource configuration will be unavailable.");
|
||||||
|
}
|
||||||
|
|
||||||
// Apply .section-last class to last section in each stack
|
// Apply .section-last class to last section in each stack
|
||||||
// (Unity UI Toolkit doesn't support :last-child pseudo-class)
|
// (Unity UI Toolkit doesn't support :last-child pseudo-class)
|
||||||
ApplySectionLastClasses();
|
ApplySectionLastClasses();
|
||||||
|
|
@ -315,6 +349,22 @@ namespace MCPForUnity.Editor.Windows
|
||||||
toolsSection.Refresh();
|
toolsSection.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureResourcesLoaded()
|
||||||
|
{
|
||||||
|
if (resourcesLoaded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourcesSection == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resourcesLoaded = true;
|
||||||
|
resourcesSection.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies the .section-last class to the last .section element in each .section-stack container.
|
/// Applies the .section-last class to the last .section element in each .section-stack container.
|
||||||
/// This is a workaround for Unity UI Toolkit not supporting the :last-child pseudo-class.
|
/// This is a workaround for Unity UI Toolkit not supporting the :last-child pseudo-class.
|
||||||
|
|
@ -355,6 +405,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
OpenWindows.Remove(this);
|
OpenWindows.Remove(this);
|
||||||
guiCreated = false;
|
guiCreated = false;
|
||||||
toolsLoaded = false;
|
toolsLoaded = false;
|
||||||
|
resourcesLoaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnFocus()
|
private void OnFocus()
|
||||||
|
|
@ -411,11 +462,13 @@ namespace MCPForUnity.Editor.Windows
|
||||||
validationTabToggle = rootVisualElement.Q<ToolbarToggle>("validation-tab");
|
validationTabToggle = rootVisualElement.Q<ToolbarToggle>("validation-tab");
|
||||||
advancedTabToggle = rootVisualElement.Q<ToolbarToggle>("advanced-tab");
|
advancedTabToggle = rootVisualElement.Q<ToolbarToggle>("advanced-tab");
|
||||||
toolsTabToggle = rootVisualElement.Q<ToolbarToggle>("tools-tab");
|
toolsTabToggle = rootVisualElement.Q<ToolbarToggle>("tools-tab");
|
||||||
|
resourcesTabToggle = rootVisualElement.Q<ToolbarToggle>("resources-tab");
|
||||||
|
|
||||||
clientsPanel?.RemoveFromClassList("hidden");
|
clientsPanel?.RemoveFromClassList("hidden");
|
||||||
validationPanel?.RemoveFromClassList("hidden");
|
validationPanel?.RemoveFromClassList("hidden");
|
||||||
advancedPanel?.RemoveFromClassList("hidden");
|
advancedPanel?.RemoveFromClassList("hidden");
|
||||||
toolsPanel?.RemoveFromClassList("hidden");
|
toolsPanel?.RemoveFromClassList("hidden");
|
||||||
|
resourcesPanel?.RemoveFromClassList("hidden");
|
||||||
|
|
||||||
if (clientsTabToggle != null)
|
if (clientsTabToggle != null)
|
||||||
{
|
{
|
||||||
|
|
@ -449,6 +502,14 @@ namespace MCPForUnity.Editor.Windows
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resourcesTabToggle != null)
|
||||||
|
{
|
||||||
|
resourcesTabToggle.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
if (evt.newValue) SwitchPanel(ActivePanel.Resources);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var savedPanel = EditorPrefs.GetString(EditorPrefKeys.EditorWindowActivePanel, ActivePanel.Clients.ToString());
|
var savedPanel = EditorPrefs.GetString(EditorPrefKeys.EditorWindowActivePanel, ActivePanel.Clients.ToString());
|
||||||
if (!Enum.TryParse(savedPanel, out ActivePanel initialPanel))
|
if (!Enum.TryParse(savedPanel, out ActivePanel initialPanel))
|
||||||
{
|
{
|
||||||
|
|
@ -481,6 +542,11 @@ namespace MCPForUnity.Editor.Windows
|
||||||
toolsPanel.style.display = DisplayStyle.None;
|
toolsPanel.style.display = DisplayStyle.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resourcesPanel != null)
|
||||||
|
{
|
||||||
|
resourcesPanel.style.display = DisplayStyle.None;
|
||||||
|
}
|
||||||
|
|
||||||
// Show selected panel
|
// Show selected panel
|
||||||
switch (panel)
|
switch (panel)
|
||||||
{
|
{
|
||||||
|
|
@ -497,6 +563,10 @@ namespace MCPForUnity.Editor.Windows
|
||||||
if (toolsPanel != null) toolsPanel.style.display = DisplayStyle.Flex;
|
if (toolsPanel != null) toolsPanel.style.display = DisplayStyle.Flex;
|
||||||
EnsureToolsLoaded();
|
EnsureToolsLoaded();
|
||||||
break;
|
break;
|
||||||
|
case ActivePanel.Resources:
|
||||||
|
if (resourcesPanel != null) resourcesPanel.style.display = DisplayStyle.Flex;
|
||||||
|
EnsureResourcesLoaded();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update toggle states
|
// Update toggle states
|
||||||
|
|
@ -504,6 +574,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
validationTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Validation);
|
validationTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Validation);
|
||||||
advancedTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Advanced);
|
advancedTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Advanced);
|
||||||
toolsTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Tools);
|
toolsTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Tools);
|
||||||
|
resourcesTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Resources);
|
||||||
|
|
||||||
EditorPrefs.SetString(EditorPrefKeys.EditorWindowActivePanel, panel.ToString());
|
EditorPrefs.SetString(EditorPrefKeys.EditorWindowActivePanel, panel.ToString());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<uie:Toolbar name="tab-toolbar" class="tab-toolbar">
|
<uie:Toolbar name="tab-toolbar" class="tab-toolbar">
|
||||||
<uie:ToolbarToggle name="clients-tab" text="Connect" value="true" />
|
<uie:ToolbarToggle name="clients-tab" text="Connect" value="true" />
|
||||||
<uie:ToolbarToggle name="tools-tab" text="Tools" />
|
<uie:ToolbarToggle name="tools-tab" text="Tools" />
|
||||||
|
<uie:ToolbarToggle name="resources-tab" text="Resources" />
|
||||||
<uie:ToolbarToggle name="validation-tab" text="Scripts" />
|
<uie:ToolbarToggle name="validation-tab" text="Scripts" />
|
||||||
<uie:ToolbarToggle name="advanced-tab" text="Advanced" />
|
<uie:ToolbarToggle name="advanced-tab" text="Advanced" />
|
||||||
</uie:Toolbar>
|
</uie:Toolbar>
|
||||||
|
|
@ -25,5 +26,8 @@
|
||||||
<ui:ScrollView name="tools-panel" class="panel-scroll hidden" style="flex-grow: 1;">
|
<ui:ScrollView name="tools-panel" class="panel-scroll hidden" style="flex-grow: 1;">
|
||||||
<ui:VisualElement name="tools-container" class="section-stack" />
|
<ui:VisualElement name="tools-container" class="section-stack" />
|
||||||
</ui:ScrollView>
|
</ui:ScrollView>
|
||||||
|
<ui:ScrollView name="resources-panel" class="panel-scroll hidden" style="flex-grow: 1;">
|
||||||
|
<ui:VisualElement name="resources-container" class="section-stack" />
|
||||||
|
</ui:ScrollView>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
</ui:UXML>
|
</ui:UXML>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from .models import MCPResponse, UnityInstanceInfo
|
from .models import MCPResponse, UnityInstanceInfo
|
||||||
from .unity_response import normalize_unity_response
|
from .unity_response import normalize_unity_response, parse_resource_response
|
||||||
|
|
||||||
__all__ = ['MCPResponse', 'UnityInstanceInfo', 'normalize_unity_response']
|
__all__ = ['MCPResponse', 'UnityInstanceInfo', 'normalize_unity_response', 'parse_resource_response']
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
"""Utilities for normalizing Unity transport responses."""
|
"""Utilities for normalizing Unity transport responses."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, Type
|
||||||
|
|
||||||
|
from models.models import MCPResponse
|
||||||
|
|
||||||
|
|
||||||
def normalize_unity_response(response: Any) -> Any:
|
def normalize_unity_response(response: Any) -> Any:
|
||||||
|
|
@ -45,3 +47,24 @@ def normalize_unity_response(response: Any) -> Any:
|
||||||
normalized["error"] = message or "Unity command failed"
|
normalized["error"] = message or "Unity command failed"
|
||||||
|
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def parse_resource_response(response: Any, typed_cls: Type[MCPResponse]) -> MCPResponse:
|
||||||
|
"""Parse a Unity response into a typed response class.
|
||||||
|
|
||||||
|
Returns a base ``MCPResponse`` for error responses so that typed subclasses
|
||||||
|
with strict ``data`` fields (e.g. ``list[str]``) don't raise Pydantic
|
||||||
|
validation errors when ``data`` is ``None``.
|
||||||
|
"""
|
||||||
|
if not isinstance(response, dict):
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Detect errors from both normalized (success=False) and raw (status="error") shapes.
|
||||||
|
if response.get("success") is False or response.get("status") == "error":
|
||||||
|
return MCPResponse(
|
||||||
|
success=False,
|
||||||
|
error=response.get("error"),
|
||||||
|
message=response.get("message"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return typed_cls(**response)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from pydantic import BaseModel
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
|
|
||||||
from models import MCPResponse
|
from models import MCPResponse
|
||||||
|
from models.unity_response import parse_resource_response
|
||||||
from services.registry import mcp_for_unity_resource
|
from services.registry import mcp_for_unity_resource
|
||||||
from services.tools import get_unity_instance_from_context
|
from services.tools import get_unity_instance_from_context
|
||||||
from transport.unity_transport import send_with_unity_instance
|
from transport.unity_transport import send_with_unity_instance
|
||||||
|
|
@ -44,4 +45,4 @@ async def get_active_tool(ctx: Context) -> ActiveToolResponse | MCPResponse:
|
||||||
"get_active_tool",
|
"get_active_tool",
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
return ActiveToolResponse(**response) if isinstance(response, dict) else response
|
return parse_resource_response(response, ActiveToolResponse)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
|
|
||||||
from models import MCPResponse
|
from models import MCPResponse
|
||||||
|
from models.unity_response import parse_resource_response
|
||||||
from services.registry import mcp_for_unity_resource
|
from services.registry import mcp_for_unity_resource
|
||||||
from services.tools import get_unity_instance_from_context
|
from services.tools import get_unity_instance_from_context
|
||||||
from transport.unity_transport import send_with_unity_instance
|
from transport.unity_transport import send_with_unity_instance
|
||||||
|
|
@ -26,4 +27,4 @@ async def get_layers(ctx: Context) -> LayersResponse | MCPResponse:
|
||||||
"get_layers",
|
"get_layers",
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
return LayersResponse(**response) if isinstance(response, dict) else response
|
return parse_resource_response(response, LayersResponse)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
|
|
||||||
from models import MCPResponse
|
from models import MCPResponse
|
||||||
|
from models.unity_response import parse_resource_response
|
||||||
from services.registry import mcp_for_unity_resource
|
from services.registry import mcp_for_unity_resource
|
||||||
from services.tools import get_unity_instance_from_context
|
from services.tools import get_unity_instance_from_context
|
||||||
from transport.unity_transport import send_with_unity_instance
|
from transport.unity_transport import send_with_unity_instance
|
||||||
|
|
@ -31,4 +32,4 @@ async def get_menu_items(ctx: Context) -> GetMenuItemsResponse | MCPResponse:
|
||||||
"get_menu_items",
|
"get_menu_items",
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
return GetMenuItemsResponse(**response) if isinstance(response, dict) else response
|
return parse_resource_response(response, GetMenuItemsResponse)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from pydantic import BaseModel
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
|
|
||||||
from models import MCPResponse
|
from models import MCPResponse
|
||||||
|
from models.unity_response import parse_resource_response
|
||||||
from services.registry import mcp_for_unity_resource
|
from services.registry import mcp_for_unity_resource
|
||||||
from services.tools import get_unity_instance_from_context
|
from services.tools import get_unity_instance_from_context
|
||||||
from transport.unity_transport import send_with_unity_instance
|
from transport.unity_transport import send_with_unity_instance
|
||||||
|
|
@ -36,4 +37,4 @@ async def get_prefab_stage(ctx: Context) -> PrefabStageResponse | MCPResponse:
|
||||||
"get_prefab_stage",
|
"get_prefab_stage",
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
return PrefabStageResponse(**response) if isinstance(response, dict) else response
|
return parse_resource_response(response, PrefabStageResponse)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from pydantic import BaseModel
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
|
|
||||||
from models import MCPResponse
|
from models import MCPResponse
|
||||||
|
from models.unity_response import parse_resource_response
|
||||||
from services.registry import mcp_for_unity_resource
|
from services.registry import mcp_for_unity_resource
|
||||||
from services.tools import get_unity_instance_from_context
|
from services.tools import get_unity_instance_from_context
|
||||||
from transport.unity_transport import send_with_unity_instance
|
from transport.unity_transport import send_with_unity_instance
|
||||||
|
|
@ -36,4 +37,4 @@ async def get_project_info(ctx: Context) -> ProjectInfoResponse | MCPResponse:
|
||||||
"get_project_info",
|
"get_project_info",
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
return ProjectInfoResponse(**response) if isinstance(response, dict) else response
|
return parse_resource_response(response, ProjectInfoResponse)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from pydantic import BaseModel
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
|
|
||||||
from models import MCPResponse
|
from models import MCPResponse
|
||||||
|
from models.unity_response import parse_resource_response
|
||||||
from services.registry import mcp_for_unity_resource
|
from services.registry import mcp_for_unity_resource
|
||||||
from services.tools import get_unity_instance_from_context
|
from services.tools import get_unity_instance_from_context
|
||||||
from transport.unity_transport import send_with_unity_instance
|
from transport.unity_transport import send_with_unity_instance
|
||||||
|
|
@ -52,4 +53,4 @@ async def get_selection(ctx: Context) -> SelectionResponse | MCPResponse:
|
||||||
"get_selection",
|
"get_selection",
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
return SelectionResponse(**response) if isinstance(response, dict) else response
|
return parse_resource_response(response, SelectionResponse)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from pydantic import Field
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
|
|
||||||
from models import MCPResponse
|
from models import MCPResponse
|
||||||
|
from models.unity_response import parse_resource_response
|
||||||
from services.registry import mcp_for_unity_resource
|
from services.registry import mcp_for_unity_resource
|
||||||
from services.tools import get_unity_instance_from_context
|
from services.tools import get_unity_instance_from_context
|
||||||
from transport.unity_transport import send_with_unity_instance
|
from transport.unity_transport import send_with_unity_instance
|
||||||
|
|
@ -27,4 +28,4 @@ async def get_tags(ctx: Context) -> TagsResponse | MCPResponse:
|
||||||
"get_tags",
|
"get_tags",
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
return TagsResponse(**response) if isinstance(response, dict) else response
|
return parse_resource_response(response, TagsResponse)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from pydantic import BaseModel, Field
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
|
|
||||||
from models import MCPResponse
|
from models import MCPResponse
|
||||||
|
from models.unity_response import parse_resource_response
|
||||||
from services.registry import mcp_for_unity_resource
|
from services.registry import mcp_for_unity_resource
|
||||||
from services.tools import get_unity_instance_from_context
|
from services.tools import get_unity_instance_from_context
|
||||||
from transport.unity_transport import send_with_unity_instance
|
from transport.unity_transport import send_with_unity_instance
|
||||||
|
|
@ -53,7 +54,7 @@ async def get_tests(ctx: Context) -> GetTestsResponse | MCPResponse:
|
||||||
"get_tests",
|
"get_tests",
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
return GetTestsResponse(**response) if isinstance(response, dict) else response
|
return parse_resource_response(response, GetTestsResponse)
|
||||||
|
|
||||||
|
|
||||||
@mcp_for_unity_resource(
|
@mcp_for_unity_resource(
|
||||||
|
|
@ -84,4 +85,4 @@ async def get_tests_for_mode(
|
||||||
"get_tests_for_mode",
|
"get_tests_for_mode",
|
||||||
{"mode": mode},
|
{"mode": mode},
|
||||||
)
|
)
|
||||||
return GetTestsResponse(**response) if isinstance(response, dict) else response
|
return parse_resource_response(response, GetTestsResponse)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from pydantic import BaseModel
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
|
|
||||||
from models import MCPResponse
|
from models import MCPResponse
|
||||||
|
from models.unity_response import parse_resource_response
|
||||||
from services.registry import mcp_for_unity_resource
|
from services.registry import mcp_for_unity_resource
|
||||||
from services.tools import get_unity_instance_from_context
|
from services.tools import get_unity_instance_from_context
|
||||||
from transport.unity_transport import send_with_unity_instance
|
from transport.unity_transport import send_with_unity_instance
|
||||||
|
|
@ -44,4 +45,4 @@ async def get_windows(ctx: Context) -> WindowsResponse | MCPResponse:
|
||||||
"get_windows",
|
"get_windows",
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
return WindowsResponse(**response) if isinstance(response, dict) else response
|
return parse_resource_response(response, WindowsResponse)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue