chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:49:17 +08:00
commit 7243d5823b
2201 changed files with 257291 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")]
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be61633e00d934610ac1ff8192ffbe3d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c9d47f01d06964ee7843765d1bd71205
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 59ff83375c2c74c8385c4a22549778dd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class AntigravityConfigurator : JsonFileMcpConfigurator
{
// Antigravity 2.x migrated its MCP config from ~/.gemini/antigravity/mcp_config.json
// (where Antigravity also stores its own runtime state — conversations, scratch, etc.)
// to a dedicated ~/.gemini/config/mcp_config.json. The migration drops a `.migrated`
// marker in the new location and renames the previous folder to `antigravity-backup`.
// The old path is no longer read by Antigravity at all, so writing there silently
// fails to register UnityMCP on every modern install.
public AntigravityConfigurator() : base(new McpClient
{
name = "Antigravity 2.0",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "config", "mcp_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "config", "mcp_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "config", "mcp_config.json"),
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
})
{ }
// Detect Antigravity itself, not just its config dir. ~/.gemini/config/ is created on
// first launch of Antigravity 2.x, so the inherited ParentDirectoryExists check
// false-negatives on a fresh install where the user hasn't opened Antigravity yet.
// ~/.antigravity/ is created by the installer (VS-Code-style support dir) and the
// macOS app bundle is dropped by the installer; either is conclusive evidence that
// Antigravity is installed, regardless of whether it has been launched.
public override bool IsInstalled
{
get
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (Directory.Exists(Path.Combine(home, ".antigravity"))) return true;
if (Directory.Exists(Path.Combine(home, ".gemini", "config"))) return true;
if (Directory.Exists(Path.Combine(home, ".gemini", "antigravity"))) return true;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return Directory.Exists("/Applications/Antigravity.app");
return false;
}
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Antigravity 2.0",
"Click the more_horiz menu in the Agent pane > MCP Servers",
"Select 'Install' for Unity MCP or use the Configure button above",
"Restart Antigravity if necessary"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 331b33961513042e3945d0a1d06615b5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Antigravity IDE — the separate IDE build that ships its own ~/.gemini/antigravity-ide/
/// runtime dir and reads its MCP config from that same folder. It did NOT migrate to
/// ~/.gemini/config/ the way Antigravity 2.0 did, so it still uses the legacy in-folder
/// mcp_config.json layout. The two apps coexist on the same machine, so we expose them
/// as separate clients rather than trying to autodetect which one to write to.
/// </summary>
public class AntigravityIdeConfigurator : JsonFileMcpConfigurator
{
public AntigravityIdeConfigurator() : base(new McpClient
{
name = "Antigravity IDE",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity-ide", "mcp_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity-ide", "mcp_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity-ide", "mcp_config.json"),
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
})
{ }
// ~/.gemini/antigravity-ide/ is created by the IDE on first launch and holds both
// its runtime state (annotations/, brain/, conversations/, ...) and its mcp_config.json
// — presence of the dir is the canonical "Antigravity IDE has been installed and
// launched at least once" signal.
public override bool IsInstalled
{
get
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Directory.Exists(Path.Combine(home, ".gemini", "antigravity-ide"));
}
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Antigravity IDE",
"Click the more_horiz menu in the Agent pane > MCP Servers",
"Select 'Install' for Unity MCP or use the Configure button above",
"Restart Antigravity IDE if necessary"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c74a24c69bb0481682d5e35731cba6b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Services;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CherryStudioConfigurator : JsonFileMcpConfigurator
{
public const string ClientName = "Cherry Studio";
public CherryStudioConfigurator() : base(new McpClient
{
name = ClientName,
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Cherry Studio", "config"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Cherry Studio", "config"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Cherry Studio", "config"),
SupportsHttpTransport = false
})
{ }
public override bool SupportsAutoConfigure => false;
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Cherry Studio",
"Go to Settings (⚙️) → MCP Server",
"Click 'Add Server' button",
"For STDIO mode (recommended):",
" - Name: unity-mcp",
" - Type: STDIO",
" - Command: uvx",
" - Arguments: Copy from the Manual Configuration JSON below",
"Click Save and restart Cherry Studio",
"",
"Note: Cherry Studio uses UI-based configuration.",
"Use the manual snippet below as reference for the values to enter."
};
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
client.SetStatus(McpStatus.NotConfigured, "Cherry Studio requires manual UI configuration");
return client.status;
}
public override void Configure()
{
throw new InvalidOperationException(
"Cherry Studio uses UI-based configuration. " +
"Please use the Manual Configuration snippet and Installation Steps to configure manually."
);
}
public override string GetManualSnippet()
{
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
if (useHttp)
{
return "# Cherry Studio does not support WebSocket transport.\n" +
"# Cherry Studio supports STDIO and SSE transports.\n" +
"# \n" +
"# To use Cherry Studio:\n" +
"# 1. Switch transport to 'Stdio' in Advanced Settings below\n" +
"# 2. Return to this configuration screen\n" +
"# 3. Copy the STDIO configuration snippet that will appear\n" +
"# \n" +
"# OPTION 2: SSE mode (future support)\n" +
"# Note: Unity MCP does not currently have an SSE endpoint.\n" +
"# This may be added in a future update.";
}
return base.GetManualSnippet() + "\n\n" +
"# Cherry Studio Configuration Instructions:\n" +
"# Cherry Studio uses UI-based configuration, not a JSON file.\n" +
"# \n" +
"# To configure:\n" +
"# 1. Open Cherry Studio\n" +
"# 2. Go to Settings (⚙️) → MCP Server\n" +
"# 3. Click 'Add Server'\n" +
"# 4. Enter the following values from the JSON above:\n" +
"# - Name: unity-mcp\n" +
"# - Type: STDIO\n" +
"# - Command: (copy 'command' value from JSON)\n" +
"# - Arguments: (copy 'args' array values, space-separated or as individual entries)\n" +
"# - Active: true\n" +
"# 5. Click Save\n" +
"# 6. Restart Cherry Studio";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6de06c6bb0399154d840a1e4c84be869
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Claude Code configurator using the CLI-based registration (claude mcp add/remove).
/// This integrates with Claude Code's native MCP management.
/// </summary>
public class ClaudeCodeConfigurator : ClaudeCliMcpConfigurator
{
public ClaudeCodeConfigurator() : base(new McpClient
{
name = "Claude Code",
SupportsHttpTransport = true,
})
{ }
public override bool SupportsSkills => true;
public override string GetSkillInstallPath()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userHome, ".claude", "skills", "unity-mcp-skill");
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Claude CLI is installed (comes with Claude Code)",
"Click Configure to add UnityMCP via 'claude mcp add'",
"The server will be automatically available in Claude Code",
"Use Unregister to remove via 'claude mcp remove'"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d0d22681fc594475db1c189f2d9abdf7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class ClaudeDesktopConfigurator : JsonFileMcpConfigurator
{
public const string ClientName = "Claude Desktop";
public ClaudeDesktopConfigurator() : base(new McpClient
{
name = ClientName,
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Claude", "claude_desktop_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Claude", "claude_desktop_config.json"),
SupportsHttpTransport = false,
StripEnvWhenNotRequired = true
})
{ }
public override bool SupportsSkills => true;
public override string GetSkillInstallPath()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userHome, ".claude", "skills", "unity-mcp-skill");
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Claude Desktop",
"Go to Settings > Developer > Edit Config\nOR open the config path",
"Paste the configuration JSON",
"Save and restart Claude Desktop"
};
private static readonly ConfiguredTransport[] StdioOnly = { ConfiguredTransport.Stdio };
public override IReadOnlyList<ConfiguredTransport> SupportedTransports => StdioOnly;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d5e5d87c9db57495f842dc366f1ebd65
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class ClineConfigurator : JsonFileMcpConfigurator
{
public ClineConfigurator() : base(new McpClient
{
name = "Cline",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
HttpTypeValue = "streamableHttp",
DefaultUnityFields = { { "disabled", false }, { "autoApprove", new object[] { } } }
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Cline in VS Code",
"Click the MCP Servers icon in the Cline pane",
"Go to Configure tab and click 'Configure MCP Servers'\nOR open the config file at the path above",
"Paste the configuration JSON into the mcpServers object",
"Save and restart VS Code"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b8abf0951c7413d9ff97a053b0adf2d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Configures the CodeBuddy CLI (~/.codebuddy.json) MCP settings.
/// </summary>
public class CodeBuddyCliConfigurator : JsonFileMcpConfigurator
{
public CodeBuddyCliConfigurator() : base(new McpClient
{
name = "CodeBuddy CLI",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"),
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install CodeBuddy CLI and ensure '~/.codebuddy.json' exists",
"Click Configure to add the UnityMCP entry (or manually edit the file above)",
"Restart your CLI session if needed"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 923728a98c8c74cfaa6e9203c408f34e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CodexConfigurator : CodexMcpConfigurator
{
public CodexConfigurator() : base(new McpClient
{
name = "Codex",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml")
})
{ }
public override bool SupportsSkills => true;
public override string GetSkillInstallPath()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userHome, ".codex", "skills", "unity-mcp-skill");
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Run 'codex config edit' in a terminal\nOR open the config file at the path above",
"Paste the configuration TOML",
"Save and restart Codex"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7037ef8b168e49f79247cb31c3be75a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CopilotCliConfigurator : JsonFileMcpConfigurator
{
public CopilotCliConfigurator() : base(new McpClient
{
name = "GitHub Copilot CLI",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json")
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot CLI (https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)",
"Open or create mcp-config.json at the path above",
"Paste the configuration JSON (or use /mcp add in the CLI)",
"Restart your Copilot CLI session"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 14a4b9a7f749248d496466c2a3a53e56
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CursorConfigurator : JsonFileMcpConfigurator
{
public CursorConfigurator() : base(new McpClient
{
name = "Cursor",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json")
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Cursor",
"Go to File > Preferences > Cursor Settings > MCP > Add new global MCP server\nOR open the config file at the path above",
"Paste the configuration JSON",
"Save and restart Cursor"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b708eda314746481fb8f4a1fb0652b03
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class GeminiCliConfigurator : JsonFileMcpConfigurator
{
public GeminiCliConfigurator() : base(new McpClient
{
name = "Gemini CLI",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "settings.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "settings.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "settings.json"),
HttpUrlProperty = "httpUrl",
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Gemini CLI is installed (see https://geminicli.com/docs/get-started/installation/)",
"Click Register to add UnityMCP via 'gemini mcp add'",
"The server will be automatically available in Gemini CLI",
"Use Unregister to remove via 'gemini mcp remove'"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c5e9bbb45e552453ab5cb557a22d43e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class KiloCodeConfigurator : JsonFileMcpConfigurator
{
public KiloCodeConfigurator() : base(new McpClient
{
name = "Kilo Code",
// Kilo Code v7.0.33+ moved MCP config out of the VS Code extension's
// globalStorage/mcp_settings.json to a CLI-style kilo.jsonc under ~/.config/kilo.
// The new schema (https://app.kilo.ai/config.json) uses an "mcp" container,
// type:"remote" for HTTP servers, type:"local" for stdio, and an "enabled" flag.
// ~/.config/kilo/kilo.jsonc on every OS (UserProfile resolves to C:\Users\<user> on Windows).
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kilo", "kilo.jsonc"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kilo", "kilo.jsonc"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kilo", "kilo.jsonc"),
IsVsCodeLayout = false,
ServerContainerKey = "mcp",
HttpTypeValue = "remote",
StdioTypeValue = "local",
SchemaUrl = "https://app.kilo.ai/config.json",
DefaultUnityFields = { { "enabled", true } }
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install or update Kilo Code (v7.0.33 or newer)",
"Open the Kilo Code MCP Servers view\nOR edit the config file at the path above (~/.config/kilo/kilo.jsonc)",
"Paste the configuration JSON into the \"mcp\" object",
"Save and restart Kilo Code"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3286d62ffe5644f5ea60488fd7e6513d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Kimi Code CLI MCP client configurator.
/// Kimi Code uses a JSON-based configuration file with mcpServers section.
/// Config path: ~/.kimi/mcp.json
///
/// Kimi Code supports both stdio (uvx) and HTTP transport modes.
/// Default: stdio mode (works without Unity Editor for basic operations)
/// HTTP mode: requires Unity Editor running with MCP HTTP server started
/// </summary>
public class KimiCodeConfigurator : JsonFileMcpConfigurator
{
public KimiCodeConfigurator() : base(new McpClient
{
name = "Kimi Code",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kimi", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kimi", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kimi", "mcp.json"),
SupportsHttpTransport = true,
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Kimi Code CLI is installed (pip install kimi-cli or see https://github.com/MoonshotAI/kimi-cli)",
"Click 'Auto Configure' to automatically add UnityMCP to ~/.kimi/mcp.json",
"OR click 'Manual Setup' to copy the configuration JSON",
"Open ~/.kimi/mcp.json and paste the configuration",
"Save and restart Kimi Code CLI",
"Use 'kimi mcp list' to verify Unity MCP is connected",
"Note: For full functionality, open Unity Editor and start HTTP server"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 562758226f6844438b5b1ae03a2cc7b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class KiroConfigurator : JsonFileMcpConfigurator
{
public KiroConfigurator() : base(new McpClient
{
name = "Kiro",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
EnsureEnvObject = true,
DefaultUnityFields = { { "disabled", false } }
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Kiro",
"Go to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config\nOR open the config file at the path above",
"Paste the configuration JSON",
"Save and restart Kiro"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e9b73ff071a6043dda1f2ec7d682ef71
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,398 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Configurator for OpenClaw via the openclaw-mcp-bridge plugin.
/// OpenClaw stores config at ~/.openclaw/openclaw.json.
/// </summary>
public class OpenClawConfigurator : McpClientConfiguratorBase
{
private const string PluginName = "openclaw-mcp-bridge";
private const string ServerName = "unityMCP";
private const string HttpTransportName = "http";
private const string StdioTransportName = "stdio";
private const string StdioUrl = "stdio://local";
public OpenClawConfigurator() : base(new McpClient
{
name = "OpenClaw",
windowsConfigPath = BuildConfigPath(),
macConfigPath = BuildConfigPath(),
linuxConfigPath = BuildConfigPath()
})
{ }
private static string BuildConfigPath()
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".openclaw",
"openclaw.json");
}
public override string GetConfigPath() => CurrentOsPath();
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
try
{
string path = GetConfigPath();
if (!File.Exists(path))
{
client.SetStatus(McpStatus.NotConfigured);
client.configuredTransport = ConfiguredTransport.Unknown;
return client.status;
}
JObject root = LoadConfig(path);
JObject pluginEntry = root["plugins"]?["entries"]?[PluginName] as JObject;
JObject unityServer = FindUnityServer(pluginEntry?["config"]?["servers"]);
if (pluginEntry == null || unityServer == null)
{
client.SetStatus(McpStatus.MissingConfig);
client.configuredTransport = ConfiguredTransport.Unknown;
return client.status;
}
if (!IsEnabled(pluginEntry) || !IsEnabled(unityServer))
{
client.SetStatus(McpStatus.NotConfigured);
client.configuredTransport = ConfiguredTransport.Unknown;
return client.status;
}
bool matches = ServerMatchesCurrentEndpoint(unityServer);
if (matches)
{
client.SetStatus(McpStatus.Configured);
client.configuredTransport = ResolveTransport(unityServer);
return client.status;
}
if (attemptAutoRewrite)
{
Configure();
}
else
{
client.SetStatus(McpStatus.IncorrectPath);
client.configuredTransport = ConfiguredTransport.Unknown;
}
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
client.configuredTransport = ConfiguredTransport.Unknown;
}
return client.status;
}
public override void Configure()
{
if (EditorPrefs.GetBool(EditorPrefKeys.LockCursorConfig, false))
return;
string path = GetConfigPath();
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
JObject root = File.Exists(path) ? LoadConfig(path) : new JObject();
JObject plugins = root["plugins"] as JObject ?? new JObject();
root["plugins"] = plugins;
JObject entries = plugins["entries"] as JObject ?? new JObject();
plugins["entries"] = entries;
JObject pluginEntry = entries[PluginName] as JObject ?? new JObject();
entries[PluginName] = pluginEntry;
pluginEntry["enabled"] = true;
JObject pluginConfig = pluginEntry["config"] as JObject ?? new JObject();
pluginEntry["config"] = pluginConfig;
pluginConfig.Remove("timeout"); // removed in openclaw-mcp-bridge v2+
pluginConfig.Remove("retries"); // removed in openclaw-mcp-bridge v2+
pluginConfig["servers"] = UpsertUnityServer(pluginConfig["servers"]);
McpConfigurationHelper.WriteAtomicFile(path, root.ToString(Formatting.Indented));
client.SetStatus(McpStatus.Configured);
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
}
public override string GetManualSnippet()
{
JObject snippet = new JObject
{
["plugins"] = new JObject
{
["entries"] = new JObject
{
[PluginName] = new JObject
{
["enabled"] = true,
["config"] = new JObject
{
["servers"] = new JObject
{
[ServerName] = BuildUnityServerEntry()
}
}
}
}
}
};
return snippet.ToString(Formatting.Indented);
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install OpenClaw",
"Install the bridge plugin: npm install -g openclaw-mcp-bridge (or pnpm add -g openclaw-mcp-bridge)",
"In MCP for Unity, choose OpenClaw and click Configure",
"OpenClaw uses the currently selected MCP for Unity transport (HTTP or stdio)",
"OpenClaw exposes a proxy tool such as unityMCP__call for Unity MCP access",
"Restart OpenClaw if the plugin does not hot-reload the new config"
};
private JObject LoadConfig(string path)
{
string text = File.ReadAllText(path);
if (string.IsNullOrWhiteSpace(text))
{
return new JObject();
}
try
{
return JsonConvert.DeserializeObject<JObject>(text) ?? new JObject();
}
catch (JsonException ex)
{
throw new InvalidOperationException(
$"OpenClaw config contains non-JSON content and cannot be safely auto-edited: {ex.Message}");
}
}
private JObject FindUnityServer(JToken serversToken)
{
if (serversToken is JObject serverMap)
{
return serverMap[ServerName] as JObject;
}
if (serversToken is JArray legacyServers)
{
foreach (JToken token in legacyServers)
{
JObject server = token as JObject;
if (server == null)
{
continue;
}
string name = server["name"]?.ToString();
if (string.Equals(name, ServerName, StringComparison.OrdinalIgnoreCase))
{
return server;
}
}
}
return null;
}
private JObject UpsertUnityServer(JToken serversToken)
{
JObject servers = NormalizeServers(serversToken);
JObject entry = servers[ServerName] as JObject ?? new JObject();
JObject desiredEntry = BuildUnityServerEntry();
entry.Remove("name");
entry.Remove("prefix");
entry.Remove("healthCheck");
entry.Remove("command");
entry.Remove("args");
entry.Remove("env");
entry.Remove("connectTimeoutMs");
foreach (var property in desiredEntry.Properties())
{
entry[property.Name] = property.Value.DeepClone();
}
servers[ServerName] = entry;
return servers;
}
private static JObject NormalizeServers(JToken serversToken)
{
if (serversToken is JObject serverMap)
{
return serverMap;
}
var normalized = new JObject();
if (!(serversToken is JArray legacyServers))
{
return normalized;
}
foreach (JToken token in legacyServers)
{
if (!(token is JObject legacyServer))
{
continue;
}
string name = legacyServer["name"]?.ToString();
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
normalized[name] = legacyServer;
}
return normalized;
}
private static JObject BuildUnityServerEntry()
{
ConfiguredTransport transport = HttpEndpointUtility.GetCurrentServerTransport();
if (transport == ConfiguredTransport.Stdio)
{
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
if (string.IsNullOrWhiteSpace(uvxPath))
{
throw new InvalidOperationException("uvx not found. Install uv/uvx or set the override in Advanced Settings.");
}
var args = new JArray();
foreach (string value in AssetPathUtility.GetUvxDevFlagsList())
{
args.Add(value);
}
foreach (string value in AssetPathUtility.GetBetaServerFromArgsList())
{
args.Add(value);
}
args.Add(packageName);
args.Add("--transport");
args.Add("stdio");
return new JObject
{
["enabled"] = true,
["url"] = StdioUrl,
["transport"] = StdioTransportName,
["command"] = uvxPath,
["args"] = args,
["toolPrefix"] = ServerName,
["requestTimeoutMs"] = 60000,
["connectTimeoutMs"] = 15000
};
}
return new JObject
{
["enabled"] = true,
["url"] = HttpEndpointUtility.GetMcpRpcUrl(),
["transport"] = HttpTransportName,
["toolPrefix"] = ServerName,
["requestTimeoutMs"] = 30000
};
}
private bool ServerMatchesCurrentEndpoint(JObject server)
{
if (server == null)
{
return false;
}
ConfiguredTransport expectedTransport = HttpEndpointUtility.GetCurrentServerTransport();
ConfiguredTransport configuredTransport = ResolveTransport(server);
if (configuredTransport != expectedTransport)
{
return false;
}
if (configuredTransport == ConfiguredTransport.Stdio)
{
string configuredUrl = server["url"]?.ToString();
string command = server["command"]?.ToString();
if (!UrlsEqual(configuredUrl, StdioUrl) || string.IsNullOrWhiteSpace(command))
{
return false;
}
// Validate the --from package source hasn't drifted (e.g. stable vs prerelease switch)
string[] args = (server["args"] as JArray)?.ToObject<string[]>();
string configuredSource = McpConfigurationHelper.ExtractUvxUrl(args);
string expectedSource = GetExpectedPackageSourceForValidation();
if (!string.IsNullOrEmpty(configuredSource) && !string.IsNullOrEmpty(expectedSource) &&
!McpConfigurationHelper.PathsEqual(configuredSource, expectedSource))
{
return false;
}
}
else
{
string configuredUrl = server["url"]?.ToString();
if (string.IsNullOrWhiteSpace(configuredUrl) ||
(!UrlsEqual(configuredUrl, HttpEndpointUtility.GetLocalMcpRpcUrl()) &&
!UrlsEqual(configuredUrl, HttpEndpointUtility.GetRemoteMcpRpcUrl())))
{
return false;
}
}
string toolPrefix = server["toolPrefix"]?.ToString();
return string.IsNullOrWhiteSpace(toolPrefix) ||
string.Equals(toolPrefix, ServerName, StringComparison.OrdinalIgnoreCase);
}
private static bool IsEnabled(JObject entry)
{
JToken enabledToken = entry["enabled"];
return enabledToken == null || enabledToken.Type != JTokenType.Boolean || enabledToken.Value<bool>();
}
private ConfiguredTransport ResolveTransport(JObject server)
{
string configuredTransport = server?["transport"]?.ToString();
string configuredUrl = server?["url"]?.ToString();
if (string.Equals(configuredTransport, StdioTransportName, StringComparison.OrdinalIgnoreCase) ||
UrlsEqual(configuredUrl, StdioUrl))
{
return ConfiguredTransport.Stdio;
}
if (UrlsEqual(configuredUrl, HttpEndpointUtility.GetRemoteMcpRpcUrl()))
{
return ConfiguredTransport.HttpRemote;
}
if (UrlsEqual(configuredUrl, HttpEndpointUtility.GetLocalMcpRpcUrl()))
{
return ConfiguredTransport.Http;
}
return ConfiguredTransport.Unknown;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 19f29c88761345158fc766d24e7c18f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Configurator for OpenCode (opencode.ai) - a Go-based terminal AI coding assistant.
/// OpenCode uses ~/.config/opencode/opencode.json with a custom "mcp" format.
/// </summary>
public class OpenCodeConfigurator : McpClientConfiguratorBase
{
private const string ServerName = "unityMCP";
private const string SchemaUrl = "https://opencode.ai/config.json";
private const string RemoteType = "remote";
private const string LocalType = "local";
public OpenCodeConfigurator() : base(new McpClient
{
name = "OpenCode",
windowsConfigPath = BuildConfigPath(),
macConfigPath = BuildConfigPath(),
linuxConfigPath = BuildConfigPath()
})
{ }
private static string BuildConfigPath()
{
string xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
string configBase = !string.IsNullOrEmpty(xdgConfigHome)
? xdgConfigHome
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
return Path.Combine(configBase, "opencode", "opencode.json");
}
public override string GetConfigPath() => CurrentOsPath();
/// <summary>
/// Attempts to load and parse the config file.
/// Returns null if file doesn't exist or cannot be read.
/// Returns parsed JObject if valid JSON found.
/// Logs warning if file exists but contains malformed JSON.
/// </summary>
private JObject TryLoadConfig(string path)
{
if (!File.Exists(path))
return null;
string content;
try
{
content = File.ReadAllText(path);
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Failed to read config file {path}: {ex.Message}");
return null;
}
try
{
return JsonConvert.DeserializeObject<JObject>(content) ?? new JObject();
}
catch (JsonException ex)
{
// Malformed JSON - log warning and return null.
// When Configure() receives null, it will do: TryLoadConfig(path) ?? new JObject()
// This creates a fresh empty JObject, which replaces the entire file with only the unityMCP section.
// Existing config sections are lost. To preserve sections, a different recovery strategy
// (e.g., line-by-line parsing, JSON repair, or manual user intervention) would be needed.
UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Malformed JSON in {path}: {ex.Message}");
return null;
}
}
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
try
{
string path = GetConfigPath();
var config = TryLoadConfig(path);
if (config == null)
{
client.SetStatus(McpStatus.NotConfigured);
return client.status;
}
var unityMcp = config["mcp"]?[ServerName] as JObject;
if (unityMcp == null)
{
client.SetStatus(McpStatus.NotConfigured);
return client.status;
}
if (EntryMatchesCurrentTransport(unityMcp))
{
client.SetStatus(McpStatus.Configured);
}
else if (attemptAutoRewrite)
{
Configure();
}
else
{
client.SetStatus(McpStatus.IncorrectPath);
}
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
}
return client.status;
}
public override void Configure()
{
try
{
string path = GetConfigPath();
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
// Load existing config or start fresh, preserving all other properties and MCP servers
var config = TryLoadConfig(path) ?? new JObject();
// Only add $schema if creating a new file
if (!File.Exists(path))
{
config["$schema"] = SchemaUrl;
}
// Preserve existing mcp section and only update our server entry
var mcpSection = config["mcp"] as JObject ?? new JObject();
config["mcp"] = mcpSection;
mcpSection[ServerName] = BuildServerEntry();
McpConfigurationHelper.WriteAtomicFile(path, JsonConvert.SerializeObject(config, Formatting.Indented));
client.SetStatus(McpStatus.Configured);
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
}
}
public override string GetManualSnippet()
{
var snippet = new JObject
{
["mcp"] = new JObject { [ServerName] = BuildServerEntry() }
};
return JsonConvert.SerializeObject(snippet, Formatting.Indented);
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install OpenCode (https://opencode.ai)",
"Click Configure to add Unity MCP to ~/.config/opencode/opencode.json",
"Restart OpenCode",
"The Unity MCP server should be detected automatically"
};
private static JObject BuildServerEntry()
{
if (HttpEndpointUtility.GetCurrentServerTransport() == ConfiguredTransport.Stdio)
{
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
if (string.IsNullOrWhiteSpace(uvxPath))
{
throw new InvalidOperationException("uvx not found. Install uv/uvx or set the override in Advanced Settings.");
}
var command = new JArray { uvxPath };
foreach (string value in AssetPathUtility.GetUvxDevFlagsList())
{
command.Add(value);
}
foreach (string value in AssetPathUtility.GetBetaServerFromArgsList())
{
command.Add(value);
}
command.Add(packageName);
command.Add("--transport");
command.Add("stdio");
return new JObject
{
["type"] = LocalType,
["command"] = command,
["enabled"] = true
};
}
return new JObject
{
["type"] = RemoteType,
["url"] = HttpEndpointUtility.GetMcpRpcUrl(),
["enabled"] = true
};
}
private bool EntryMatchesCurrentTransport(JObject entry)
{
string entryType = entry["type"]?.ToString();
ConfiguredTransport expected = HttpEndpointUtility.GetCurrentServerTransport();
if (expected == ConfiguredTransport.Stdio)
{
return string.Equals(entryType, LocalType, StringComparison.OrdinalIgnoreCase)
&& entry["command"] is JArray;
}
return string.Equals(entryType, RemoteType, StringComparison.OrdinalIgnoreCase)
&& UrlsEqual(entry["url"]?.ToString(), HttpEndpointUtility.GetMcpRpcUrl());
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 489f99ffb7e6743e88e3203552c8b37b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Qwen Code MCP client configurator.
/// Qwen Code uses a JSON-based configuration file with mcpServers section.
/// Config path: ~/.qwen/settings.json
///
/// Qwen Code supports both stdio (uvx) and HTTP transport modes.
/// Default: stdio mode (works without Unity Editor for basic operations)
/// HTTP mode: requires Unity Editor running with MCP HTTP server started
/// </summary>
public class QwenCodeConfigurator : JsonFileMcpConfigurator
{
public QwenCodeConfigurator() : base(new McpClient
{
name = "Qwen Code",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".qwen", "settings.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".qwen", "settings.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".qwen", "settings.json"),
SupportsHttpTransport = true,
// Default to stdio transport for Qwen Code (like Cursor)
// User can switch to HTTP in Unity: Window > MCP for Unity > Settings
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Qwen Code is installed (npm install -g @qwen-code/qwen-code or download from https://github.com/QwenLM/qwen-code)",
"Open Qwen Code",
"Click 'Auto Configure' to automatically add UnityMCP to settings.json",
"OR click 'Manual Setup' to copy the configuration JSON",
"Open ~/.qwen/settings.json and paste the configuration",
"Save and restart Qwen Code",
"Use /mcp command in Qwen Code to verify Unity MCP is connected",
"Note: For full functionality, open Unity Editor and start HTTP server"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 46891bcdb00e468cbd04afbfb8f3095e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class RiderConfigurator : JsonFileMcpConfigurator
{
public RiderConfigurator() : base(new McpClient
{
name = "Rider GitHub Copilot",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "github-copilot", "intellij", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "github-copilot", "intellij", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "github-copilot", "intellij", "mcp.json"),
IsVsCodeLayout = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot plugin in Rider",
"Open or create mcp.json at the path above",
"Paste the configuration JSON",
"Save and restart Rider"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2511b0d05271d486bb61f8cc9fd11363
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class TraeConfigurator : JsonFileMcpConfigurator
{
public TraeConfigurator() : base(new McpClient
{
name = "Trae",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Trae", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Trae", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Trae", "mcp.json"),
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Trae and go to Settings > MCP",
"Select Add Server > Add Manually",
"Paste the JSON or point to the mcp.json file\n"+
"Windows: %AppData%\\Trae\\mcp.json\n" +
"macOS: ~/Library/Application Support/Trae/mcp.json\n" +
"Linux: ~/.config/Trae/mcp.json\n",
"Save and restart Trae"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3ab39e22ae0948ab94beae307f9902e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class VSCodeConfigurator : JsonFileMcpConfigurator
{
public VSCodeConfigurator() : base(new McpClient
{
name = "VSCode GitHub Copilot",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "mcp.json"),
IsVsCodeLayout = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot extension",
"Open or create mcp.json at the path above",
"Paste the configuration JSON",
"Save and restart VSCode"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bcc7ead475a4d4ea2978151c217757b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class VSCodeInsidersConfigurator : JsonFileMcpConfigurator
{
public VSCodeInsidersConfigurator() : base(new McpClient
{
name = "VSCode Insiders GitHub Copilot",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code - Insiders", "User", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code - Insiders", "User", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code - Insiders", "User", "mcp.json"),
IsVsCodeLayout = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot extension in VS Code Insiders",
"Open or create mcp.json at the path above",
"Paste the configuration JSON",
"Save and restart VS Code Insiders"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c4a1b0d3b34489cbf0f8c40c49c4f3b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class WindsurfConfigurator : JsonFileMcpConfigurator
{
public WindsurfConfigurator() : base(new McpClient
{
name = "Windsurf",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Windsurf",
"Go to File > Preferences > Windsurf Settings > MCP > Manage MCPs > View raw config\nOR open the config file at the path above",
"Paste the configuration JSON",
"Save and restart Windsurf"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b528971e189f141d38db577f155bd222
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,77 @@
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients
{
/// <summary>
/// Contract for MCP client configurators. Each client is responsible for
/// status detection, auto-configure, and manual snippet/steps.
/// </summary>
public interface IMcpClientConfigurator
{
/// <summary>Stable identifier (e.g., "cursor").</summary>
string Id { get; }
/// <summary>Display name shown in the UI.</summary>
string DisplayName { get; }
/// <summary>Current status cached by the configurator.</summary>
McpStatus Status { get; }
/// <summary>
/// The transport type the client is currently configured for.
/// Returns Unknown if the client is not configured or the transport cannot be determined.
/// </summary>
ConfiguredTransport ConfiguredTransport { get; }
/// <summary>True if this client supports auto-configure.</summary>
bool SupportsAutoConfigure { get; }
/// <summary>
/// True if this client appears installed on the user's machine. Used to filter
/// "configure all detected" so we don't write configs for apps the user doesn't have.
/// Implementations should be cheap (filesystem stat or cached path lookup).
/// </summary>
bool IsInstalled { get; }
/// <summary>
/// Transports this client can be configured with. Order is "preference if user has no opinion";
/// the configure path picks the user's global preference if present in this list, else falls back to the first entry.
/// </summary>
System.Collections.Generic.IReadOnlyList<ConfiguredTransport> SupportedTransports { get; }
/// <summary>Label to show on the configure button for the current state.</summary>
string GetConfigureActionLabel();
/// <summary>Returns the platform-specific config path (or message for CLI-managed clients).</summary>
string GetConfigPath();
/// <summary>Checks and updates status; returns current status.</summary>
McpStatus CheckStatus(bool attemptAutoRewrite = true);
/// <summary>Runs auto-configuration (register/write file/CLI etc.). Always idempotent
/// — calling twice with the same settings is safe and is what the bulk "Configure All"
/// path relies on to refresh transport / server-version drift across every detected
/// client.</summary>
void Configure();
/// <summary>
/// Removes UnityMCP from this client's config (JSON entry, CLI registration, etc.).
/// Default is a no-op for client types that don't yet implement removal (Codex TOML);
/// callers should treat this as best-effort. The UI's per-client button routes here
/// when <see cref="Status"/> is <see cref="McpStatus.Configured"/>.
/// </summary>
void Unregister();
/// <summary>Returns the manual configuration snippet (JSON/TOML/commands).</summary>
string GetManualSnippet();
/// <summary>Returns ordered human-readable installation steps.</summary>
System.Collections.Generic.IList<string> GetInstallationSteps();
/// <summary>True if this client supports skill installation/sync.</summary>
bool SupportsSkills { get; }
/// <summary>Returns the absolute path where skills should be installed, or null if unsupported.</summary>
string GetSkillInstallPath();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f5a5078d9e6e14027a1abfebf4018634
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d408fd7733cb4a1eb80f785307db2ff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Clients
{
/// <summary>
/// Central registry that auto-discovers configurators via TypeCache.
/// </summary>
public static class McpClientRegistry
{
private static List<IMcpClientConfigurator> cached;
public static IReadOnlyList<IMcpClientConfigurator> All
{
get
{
if (cached == null)
{
cached = BuildRegistry();
}
return cached;
}
}
private static List<IMcpClientConfigurator> BuildRegistry()
{
var configurators = new List<IMcpClientConfigurator>();
foreach (var type in TypeCache.GetTypesDerivedFrom<IMcpClientConfigurator>())
{
if (type.IsAbstract || !type.IsClass || !type.IsPublic)
continue;
// Require a public parameterless constructor
if (type.GetConstructor(Type.EmptyTypes) == null)
continue;
try
{
if (Activator.CreateInstance(type) is IMcpClientConfigurator instance)
{
configurators.Add(instance);
}
}
catch (Exception ex)
{
McpLog.Warn($"UnityMCP: Failed to instantiate configurator {type.Name}: {ex.Message}");
}
}
// Alphabetical order by display name
configurators = configurators.OrderBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase).ToList();
return configurators;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4ce08555f995e4e848a826c63f18cb35
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f7e009cbf3e74f6c987331c2b438ec59
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,10 @@
namespace MCPForUnity.Editor.Constants
{
/// <summary>
/// Protocol-level constants for API key authentication.
/// </summary>
internal static class AuthConstants
{
internal const string ApiKeyHeader = "X-API-Key";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96844bc39e9a94cf18b18f8127f3854f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,85 @@
namespace MCPForUnity.Editor.Constants
{
/// <summary>
/// Centralized list of EditorPrefs keys used by the MCP for Unity package.
/// Keeping them in one place avoids typos and simplifies migrations.
/// </summary>
internal static class EditorPrefKeys
{
internal const string UseHttpTransport = "MCPForUnity.UseHttpTransport";
internal const string HttpTransportScope = "MCPForUnity.HttpTransportScope"; // "local" | "remote"
internal const string LastLocalHttpServerPid = "MCPForUnity.LocalHttpServer.LastPid";
internal const string LastLocalHttpServerPort = "MCPForUnity.LocalHttpServer.LastPort";
internal const string LastLocalHttpServerStartedUtc = "MCPForUnity.LocalHttpServer.LastStartedUtc";
internal const string LastLocalHttpServerPidArgsHash = "MCPForUnity.LocalHttpServer.LastPidArgsHash";
internal const string LastLocalHttpServerPidFilePath = "MCPForUnity.LocalHttpServer.LastPidFilePath";
internal const string LastLocalHttpServerInstanceToken = "MCPForUnity.LocalHttpServer.LastInstanceToken";
internal const string DebugLogs = "MCPForUnity.DebugLogs";
internal const string ValidationLevel = "MCPForUnity.ValidationLevel";
internal const string UnitySocketPort = "MCPForUnity.UnitySocketPort";
internal const string ResumeStdioAfterReload = "MCPForUnity.ResumeStdioAfterReload";
internal const string UvxPathOverride = "MCPForUnity.UvxPath";
internal const string ClaudeCliPathOverride = "MCPForUnity.ClaudeCliPath";
internal const string ClientProjectDirOverride = "MCPForUnity.ClientProjectDir";
internal const string HttpBaseUrl = "MCPForUnity.HttpUrl";
internal const string HttpRemoteBaseUrl = "MCPForUnity.HttpRemoteUrl";
internal const string SessionId = "MCPForUnity.SessionId";
internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl";
internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride";
internal const string DevModeForceServerRefresh = "MCPForUnity.DevModeForceServerRefresh";
internal const string ProjectScopedToolsLocalHttp = "MCPForUnity.ProjectScopedTools.LocalHttp";
internal const string AllowLanHttpBind = "MCPForUnity.Security.AllowLanHttpBind";
internal const string AllowInsecureRemoteHttp = "MCPForUnity.Security.AllowInsecureRemoteHttp";
internal const string PackageDeploySourcePath = "MCPForUnity.PackageDeploy.SourcePath";
internal const string PackageDeployLastBackupPath = "MCPForUnity.PackageDeploy.LastBackupPath";
internal const string PackageDeployLastTargetPath = "MCPForUnity.PackageDeploy.LastTargetPath";
internal const string PackageDeployLastSourcePath = "MCPForUnity.PackageDeploy.LastSourcePath";
internal const string ServerSrc = "MCPForUnity.ServerSrc";
internal const string UseEmbeddedServer = "MCPForUnity.UseEmbeddedServer";
internal const string LockCursorConfig = "MCPForUnity.LockCursorConfig";
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 LastSelectedClientId = "MCPForUnity.LastSelectedClientId";
internal const string ClientDetailsFoldoutOpen = "MCPForUnity.ClientConfig.DetailsFoldoutOpen";
internal const string SetupCompleted = "MCPForUnity.SetupCompleted";
internal const string SetupDismissed = "MCPForUnity.SetupDismissed";
internal const string CustomToolRegistrationEnabled = "MCPForUnity.CustomToolRegistrationEnabled";
internal const string LastUpdateCheck = "MCPForUnity.LastUpdateCheck";
internal const string LatestKnownVersion = "MCPForUnity.LatestKnownVersion";
internal const string LastAssetStoreUpdateCheck = "MCPForUnity.LastAssetStoreUpdateCheck";
internal const string LatestKnownAssetStoreVersion = "MCPForUnity.LatestKnownAssetStoreVersion";
internal const string LastStdIoUpgradeVersion = "MCPForUnity.LastStdIoUpgradeVersion";
internal const string TelemetryDisabled = "MCPForUnity.TelemetryDisabled";
internal const string CustomerUuid = "MCPForUnity.CustomerUUID";
internal const string ApiKey = "MCPForUnity.ApiKey";
internal const string AutoStartOnLoad = "MCPForUnity.AutoStartOnLoad";
internal const string HttpServerLaunchConfirmed = "MCPForUnity.HttpServerLaunchConfirmed";
internal const string BatchExecuteMaxCommands = "MCPForUnity.BatchExecute.MaxCommands";
internal const string LogRecordEnabled = "MCPForUnity.LogRecordEnabled";
internal const string ExecuteCodeCompiler = "MCPForUnity.ExecuteCode.Compiler";
// AI Asset Generation — NON-SECRET config only. Provider API keys live in the OS
// secure store (MCPForUnity.Editor.Security.SecureKeyStore), never in EditorPrefs.
internal const string AssetGenSelectedModelProvider = "MCPForUnity.AssetGen.ModelProvider";
internal const string AssetGenSelectedImageProvider = "MCPForUnity.AssetGen.ImageProvider";
internal const string AssetGenDefaultFormat = "MCPForUnity.AssetGen.Format";
internal const string AssetGenOutputRoot = "MCPForUnity.AssetGen.OutputRoot";
internal const string AssetGenAutoNormalize = "MCPForUnity.AssetGen.AutoNormalize";
internal const string AssetGenProviderEnabledPrefix = "MCPForUnity.AssetGen.Enabled.";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7317786cfb9304b0db20ca73a774b9fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
namespace MCPForUnity.Editor.Constants
{
/// <summary>
/// Constants for health check status values.
/// Used for coordinating health state between Connection and Advanced sections.
/// </summary>
public static class HealthStatus
{
public const string Unknown = "Unknown";
public const string Healthy = "Healthy";
public const string PingFailed = "Ping Failed";
public const string Unhealthy = "Unhealthy";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c15ed2426f43860479f1b8a99a343d16
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
namespace MCPForUnity.Editor.Constants
{
/// <summary>Canonical user-facing product identity strings.</summary>
public static class ProductInfo
{
public const string ProductName = "MCP for Unity";
public const string MenuRoot = "Window/MCP for Unity";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1df5e80156ac4f1ebe343e537eaa9da4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 221a4d6e595be6897a5b17b77aedd4d0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Dependencies.PlatformDetectors;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Dependencies
{
/// <summary>
/// Main orchestrator for dependency validation and management
/// </summary>
public static class DependencyManager
{
private static readonly List<IPlatformDetector> _detectors = new List<IPlatformDetector>
{
new WindowsPlatformDetector(),
new MacOSPlatformDetector(),
new LinuxPlatformDetector()
};
private static IPlatformDetector _currentDetector;
/// <summary>
/// Get the platform detector for the current operating system
/// </summary>
public static IPlatformDetector GetCurrentPlatformDetector()
{
if (_currentDetector == null)
{
_currentDetector = _detectors.FirstOrDefault(d => d.CanDetect);
if (_currentDetector == null)
{
throw new PlatformNotSupportedException($"No detector available for current platform: {RuntimeInformation.OSDescription}");
}
}
return _currentDetector;
}
/// <summary>
/// Perform a comprehensive dependency check
/// </summary>
public static DependencyCheckResult CheckAllDependencies()
{
var result = new DependencyCheckResult();
try
{
var detector = GetCurrentPlatformDetector();
McpLog.Info($"Checking dependencies on {detector.PlatformName}...", always: false);
// Check Python
var pythonStatus = detector.DetectPython();
result.Dependencies.Add(pythonStatus);
// Check uv
var uvStatus = detector.DetectUv();
result.Dependencies.Add(uvStatus);
// Generate summary and recommendations
result.GenerateSummary();
GenerateRecommendations(result, detector);
McpLog.Info($"Dependency check completed. System ready: {result.IsSystemReady}", always: false);
}
catch (Exception ex)
{
McpLog.Error($"Error during dependency check: {ex.Message}");
result.Summary = $"Dependency check failed: {ex.Message}";
result.IsSystemReady = false;
}
return result;
}
/// <summary>
/// Get installation recommendations for the current platform
/// </summary>
public static string GetInstallationRecommendations()
{
try
{
var detector = GetCurrentPlatformDetector();
return detector.GetInstallationRecommendations();
}
catch (Exception ex)
{
return $"Error getting installation recommendations: {ex.Message}";
}
}
/// <summary>
/// Get platform-specific installation URLs
/// </summary>
public static (string pythonUrl, string uvUrl) GetInstallationUrls()
{
try
{
var detector = GetCurrentPlatformDetector();
return (detector.GetPythonInstallUrl(), detector.GetUvInstallUrl());
}
catch
{
return ("https://python.org/downloads/", "https://docs.astral.sh/uv/getting-started/installation/");
}
}
private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector)
{
var missing = result.GetMissingDependencies();
if (missing.Count == 0)
{
result.RecommendedActions.Add("All dependencies are available. You can start using MCP for Unity.");
return;
}
foreach (var dep in missing)
{
if (dep.Name == "Python")
{
result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}");
}
else if (dep.Name == "uv Package Manager")
{
result.RecommendedActions.Add($"Install uv package manager from: {detector.GetUvInstallUrl()}");
}
else if (dep.Name == "MCP Server")
{
result.RecommendedActions.Add("MCP Server will be installed automatically when needed.");
}
}
if (result.GetMissingRequired().Count > 0)
{
result.RecommendedActions.Add("Use the Setup Window (Window > MCP for Unity > Local Setup Window) for guided installation.");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4a6d2236d370b4f1db4d0e3d3ce0dcac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4c0f2e87395b4c6c9df8c21b6d0fae13
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MCPForUnity.Editor.Dependencies.Models
{
/// <summary>
/// Result of a comprehensive dependency check
/// </summary>
[Serializable]
public class DependencyCheckResult
{
/// <summary>
/// List of all dependency statuses checked
/// </summary>
public List<DependencyStatus> Dependencies { get; set; }
/// <summary>
/// Overall system readiness for MCP operations
/// </summary>
public bool IsSystemReady { get; set; }
/// <summary>
/// Whether all required dependencies are available
/// </summary>
public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false;
/// <summary>
/// Whether any optional dependencies are missing
/// </summary>
public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false;
/// <summary>
/// Summary message about the dependency state
/// </summary>
public string Summary { get; set; }
/// <summary>
/// Recommended next steps for the user
/// </summary>
public List<string> RecommendedActions { get; set; }
/// <summary>
/// Timestamp when this check was performed
/// </summary>
public DateTime CheckedAt { get; set; }
public DependencyCheckResult()
{
Dependencies = new List<DependencyStatus>();
RecommendedActions = new List<string>();
CheckedAt = DateTime.UtcNow;
}
/// <summary>
/// Get dependencies by availability status
/// </summary>
public List<DependencyStatus> GetMissingDependencies()
{
return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
}
/// <summary>
/// Get missing required dependencies
/// </summary>
public List<DependencyStatus> GetMissingRequired()
{
return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
}
/// <summary>
/// Generate a user-friendly summary of the dependency state
/// </summary>
public void GenerateSummary()
{
var missing = GetMissingDependencies();
var missingRequired = GetMissingRequired();
if (missing.Count == 0)
{
Summary = "All dependencies are available and ready.";
IsSystemReady = true;
}
else if (missingRequired.Count == 0)
{
Summary = $"System is ready. {missing.Count} optional dependencies are missing.";
IsSystemReady = true;
}
else
{
Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing.";
IsSystemReady = false;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f6df82faa423f4e9ebb13a3dcee8ba19
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,65 @@
using System;
namespace MCPForUnity.Editor.Dependencies.Models
{
/// <summary>
/// Represents the status of a dependency check
/// </summary>
[Serializable]
public class DependencyStatus
{
/// <summary>
/// Name of the dependency being checked
/// </summary>
public string Name { get; set; }
/// <summary>
/// Whether the dependency is available and functional
/// </summary>
public bool IsAvailable { get; set; }
/// <summary>
/// Version information if available
/// </summary>
public string Version { get; set; }
/// <summary>
/// Path to the dependency executable/installation
/// </summary>
public string Path { get; set; }
/// <summary>
/// Additional details about the dependency status
/// </summary>
public string Details { get; set; }
/// <summary>
/// Error message if dependency check failed
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// Whether this dependency is required for basic functionality
/// </summary>
public bool IsRequired { get; set; }
/// <summary>
/// Suggested installation method or URL
/// </summary>
public string InstallationHint { get; set; }
public DependencyStatus(string name, bool isRequired = true)
{
Name = name;
IsRequired = isRequired;
IsAvailable = false;
}
public override string ToString()
{
var status = IsAvailable ? "✓" : "✗";
var version = !string.IsNullOrEmpty(Version) ? $" ({Version})" : "";
return $"{status} {Name}{version}";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ddeeeca2f876f4083a84417404175199
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bdbaced669d14798a4ceeebfbff2b22c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,45 @@
using MCPForUnity.Editor.Dependencies.Models;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// Interface for platform-specific dependency detection
/// </summary>
public interface IPlatformDetector
{
/// <summary>
/// Platform name this detector handles
/// </summary>
string PlatformName { get; }
/// <summary>
/// Whether this detector can run on the current platform
/// </summary>
bool CanDetect { get; }
/// <summary>
/// Detect Python installation on this platform
/// </summary>
DependencyStatus DetectPython();
/// <summary>
/// Detect uv package manager on this platform
/// </summary>
DependencyStatus DetectUv();
/// <summary>
/// Get platform-specific installation recommendations
/// </summary>
string GetInstallationRecommendations();
/// <summary>
/// Get platform-specific Python installation URL
/// </summary>
string GetPythonInstallUrl();
/// <summary>
/// Get platform-specific uv installation URL
/// </summary>
string GetUvInstallUrl();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 67d73d0e8caef4e60942f4419c6b76bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,207 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// Linux-specific dependency detection
/// </summary>
public class LinuxPlatformDetector : PlatformDetectorBase
{
public override string PlatformName => "Linux";
public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
public override DependencyStatus DetectPython()
{
var status = new DependencyStatus("Python", isRequired: true)
{
InstallationHint = GetPythonInstallUrl()
};
try
{
// Try running python directly first
if (TryValidatePython("python3", out string version, out string fullPath) ||
TryValidatePython("python", out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} in PATH";
return status;
}
// Fallback: try 'which' command
if (TryFindInPath("python3", out string pathResult) ||
TryFindInPath("python", out pathResult))
{
if (TryValidatePython(pathResult, out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} in PATH";
return status;
}
}
status.ErrorMessage = "Python not found in PATH";
status.Details = "Install Python 3.10+ and ensure it's added to PATH.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting Python: {ex.Message}";
}
return status;
}
public override string GetPythonInstallUrl()
{
return "https://www.python.org/downloads/source/";
}
public override string GetUvInstallUrl()
{
return "https://docs.astral.sh/uv/getting-started/installation/#linux";
}
public override string GetInstallationRecommendations()
{
return @"Linux Installation Recommendations:
1. Python: Install via package manager or pyenv
- Ubuntu/Debian: sudo apt install python3 python3-pip
- Fedora/RHEL: sudo dnf install python3 python3-pip
- Arch: sudo pacman -S python python-pip
- Or use pyenv: https://github.com/pyenv/pyenv
2. uv Package Manager: Install via curl
- Run: curl -LsSf https://astral.sh/uv/install.sh | sh
- Or download from: https://github.com/astral-sh/uv/releases
3. MCP Server: Will be installed automatically by MCP for Unity
Note: Make sure ~/.local/bin is in your PATH for user-local installations.";
}
public override DependencyStatus DetectUv()
{
// First, honor overrides and cross-platform resolution via the base implementation
var status = base.DetectUv();
if (status.IsAvailable)
{
return status;
}
// If the user configured an override path but fallback was not used, keep the base result
// (failure typically means the override path is invalid and no system fallback found)
if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback)
{
return status;
}
try
{
string augmentedPath = BuildAugmentedPath();
// Try uv first, then uvx, using ExecPath.TryRun for proper timeout handling
if (TryValidateUvWithPath("uv", augmentedPath, out string version, out string fullPath) ||
TryValidateUvWithPath("uvx", augmentedPath, out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found uv {version} in PATH";
status.ErrorMessage = null;
return status;
}
status.ErrorMessage = "uv not found in PATH";
status.Details = "Install uv package manager and ensure it's added to PATH.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting uv: {ex.Message}";
}
return status;
}
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
string augmentedPath = BuildAugmentedPath();
// First, try to resolve the absolute path for better UI/logging display
string commandToRun = pythonPath;
if (TryFindInPath(pythonPath, out string resolvedPath))
{
commandToRun = resolvedPath;
}
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr,
5000, augmentedPath))
return false;
// Check stdout first, then stderr (some Python distributions output to stderr)
string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
if (output.StartsWith("Python "))
{
version = output.Substring(7);
fullPath = commandToRun;
if (TryParseVersion(version, out var major, out var minor))
{
return major > 3 || (major == 3 && minor >= 10);
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
protected string BuildAugmentedPath()
{
var additions = GetPathAdditions();
if (additions.Length == 0) return null;
// Only return the additions - ExecPath.TryRun will prepend to existing PATH
return string.Join(Path.PathSeparator, additions);
}
private string[] GetPathAdditions()
{
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return new[]
{
"/usr/local/bin",
"/usr/bin",
"/bin",
"/snap/bin",
Path.Combine(homeDir, ".local", "bin")
};
}
protected override bool TryFindInPath(string executable, out string fullPath)
{
fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath());
return !string.IsNullOrEmpty(fullPath);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b682b492eb80d4ed6834b76f72c9f0f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,206 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// macOS-specific dependency detection
/// </summary>
public class MacOSPlatformDetector : PlatformDetectorBase
{
public override string PlatformName => "macOS";
public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public override DependencyStatus DetectPython()
{
var status = new DependencyStatus("Python", isRequired: true)
{
InstallationHint = GetPythonInstallUrl()
};
try
{
// 1. Try 'which' command with augmented PATH (prioritizing Homebrew)
if (TryFindInPath("python3", out string pathResult) ||
TryFindInPath("python", out pathResult))
{
if (TryValidatePython(pathResult, out string version, out string fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} at {fullPath}";
return status;
}
}
// 2. Fallback: Try running python directly from PATH
if (TryValidatePython("python3", out string v, out string p) ||
TryValidatePython("python", out v, out p))
{
status.IsAvailable = true;
status.Version = v;
status.Path = p;
status.Details = $"Found Python {v} in PATH";
return status;
}
status.ErrorMessage = "Python not found in PATH or standard locations";
status.Details = "Install Python 3.10+ via Homebrew ('brew install python3') and ensure it's in your PATH.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting Python: {ex.Message}";
}
return status;
}
public override string GetPythonInstallUrl()
{
return "https://www.python.org/downloads/macos/";
}
public override string GetUvInstallUrl()
{
return "https://docs.astral.sh/uv/getting-started/installation/#macos";
}
public override string GetInstallationRecommendations()
{
return @"macOS Installation Recommendations:
1. Python: Install via Homebrew (recommended) or python.org
- Homebrew: brew install python3
- Direct download: https://python.org/downloads/macos/
2. uv Package Manager: Install via curl or Homebrew
- Curl: curl -LsSf https://astral.sh/uv/install.sh | sh
- Homebrew: brew install uv
3. MCP Server: Will be installed automatically by MCP for Unity Bridge
Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH.";
}
public override DependencyStatus DetectUv()
{
// First, honor overrides and cross-platform resolution via the base implementation
var status = base.DetectUv();
if (status.IsAvailable)
{
return status;
}
// If the user configured an override path but fallback was not used, keep the base result
// (failure typically means the override path is invalid and no system fallback found)
if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback)
{
return status;
}
try
{
string augmentedPath = BuildAugmentedPath();
// Try uv first, then uvx, using ExecPath.TryRun for proper timeout handling
if (TryValidateUvWithPath("uv", augmentedPath, out string version, out string fullPath) ||
TryValidateUvWithPath("uvx", augmentedPath, out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found uv {version} in PATH";
status.ErrorMessage = null;
return status;
}
status.ErrorMessage = "uv not found in PATH";
status.Details = "Install uv package manager and ensure it's added to PATH.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting uv: {ex.Message}";
}
return status;
}
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
string augmentedPath = BuildAugmentedPath();
// First, try to resolve the absolute path for better UI/logging display
string commandToRun = pythonPath;
if (TryFindInPath(pythonPath, out string resolvedPath))
{
commandToRun = resolvedPath;
}
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr,
5000, augmentedPath))
return false;
// Check stdout first, then stderr (some Python distributions output to stderr)
string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
if (output.StartsWith("Python "))
{
version = output.Substring(7);
fullPath = commandToRun;
if (TryParseVersion(version, out var major, out var minor))
{
return major > 3 || (major == 3 && minor >= 10);
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
protected string BuildAugmentedPath()
{
var additions = GetPathAdditions();
if (additions.Length == 0) return null;
// Only return the additions - ExecPath.TryRun will prepend to existing PATH
return string.Join(Path.PathSeparator, additions);
}
private string[] GetPathAdditions()
{
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return new[]
{
Path.Combine(homeDir, ".pyenv", "shims"), // pyenv: Python/uv when Unity is launched from Dock/Spotlight
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
"/bin",
Path.Combine(homeDir, ".local", "bin")
};
}
protected override bool TryFindInPath(string executable, out string fullPath)
{
fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath());
return !string.IsNullOrEmpty(fullPath);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c6f602b0a8ca848859197f9a949a7a5d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,137 @@
using System;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// Base class for platform-specific dependency detection
/// </summary>
public abstract class PlatformDetectorBase : IPlatformDetector
{
public abstract string PlatformName { get; }
public abstract bool CanDetect { get; }
public abstract DependencyStatus DetectPython();
public abstract string GetPythonInstallUrl();
public abstract string GetUvInstallUrl();
public abstract string GetInstallationRecommendations();
public virtual DependencyStatus DetectUv()
{
var status = new DependencyStatus("uv Package Manager", isRequired: true)
{
InstallationHint = GetUvInstallUrl()
};
try
{
// Get uv path from PathResolverService (respects override)
string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
// Verify uv executable and get version
if (MCPServiceLocator.Paths.TryValidateUvxExecutable(uvxPath, out string version))
{
status.IsAvailable = true;
status.Version = version;
status.Path = uvxPath;
// Check if we used fallback from override to system path
if (MCPServiceLocator.Paths.HasUvxPathFallback)
{
status.Details = $"Found uv {version} (fallback to system path)";
status.ErrorMessage = "Override path not found, using system path";
}
else
{
status.Details = MCPServiceLocator.Paths.HasUvxPathOverride
? $"Found uv {version} (override path)"
: $"Found uv {version} in system path";
}
return status;
}
status.ErrorMessage = "uvx not found";
status.Details = "Install uv package manager or configure path override in Advanced Settings.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting uvx: {ex.Message}";
}
return status;
}
protected bool TryParseVersion(string version, out int major, out int minor)
{
major = 0;
minor = 0;
try
{
var parts = version.Split('.');
if (parts.Length >= 2)
{
return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor);
}
}
catch
{
// Ignore parsing errors
}
return false;
}
// In PlatformDetectorBase.cs
protected bool TryValidateUvWithPath(string command, string augmentedPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
string commandToRun = command;
if (TryFindInPath(command, out string resolvedPath))
{
commandToRun = resolvedPath;
}
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr,
5000, augmentedPath))
return false;
string output = string.IsNullOrWhiteSpace(stdout) ? stderr.Trim() : stdout.Trim();
if (output.StartsWith("uvx ") || output.StartsWith("uv "))
{
int spaceIndex = output.IndexOf(' ');
if (spaceIndex >= 0)
{
var remainder = output.Substring(spaceIndex + 1).Trim();
int nextSpace = remainder.IndexOf(' ');
int parenIndex = remainder.IndexOf('(');
int endIndex = Math.Min(
nextSpace >= 0 ? nextSpace : int.MaxValue,
parenIndex >= 0 ? parenIndex : int.MaxValue
);
version = endIndex < int.MaxValue ? remainder.Substring(0, endIndex).Trim() : remainder;
fullPath = commandToRun;
return true;
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
// Add abstract method for subclasses to implement
protected abstract bool TryFindInPath(string executable, out string fullPath);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 44d715aedea2b8b41bf914433bbb2c49
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// Windows-specific dependency detection
/// </summary>
public class WindowsPlatformDetector : PlatformDetectorBase
{
public override string PlatformName => "Windows";
public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public override DependencyStatus DetectPython()
{
var status = new DependencyStatus("Python", isRequired: true)
{
InstallationHint = GetPythonInstallUrl()
};
try
{
// Try running python directly first (works with Windows App Execution Aliases)
if (TryValidatePython("python3.exe", out string version, out string fullPath) ||
TryValidatePython("python.exe", out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} in PATH";
return status;
}
// Fallback: try 'where' command
if (TryFindInPath("python3.exe", out string pathResult) ||
TryFindInPath("python.exe", out pathResult))
{
if (TryValidatePython(pathResult, out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} in PATH";
return status;
}
}
// Fallback: try to find python via uv
if (TryFindPythonViaUv(out version, out fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} via uv";
return status;
}
status.ErrorMessage = "Python not found in PATH";
status.Details = "Install Python 3.10+ and ensure it's added to PATH.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting Python: {ex.Message}";
}
return status;
}
public override string GetPythonInstallUrl()
{
return "https://apps.microsoft.com/store/detail/python-313/9NCVDN91XZQP";
}
public override string GetUvInstallUrl()
{
return "https://docs.astral.sh/uv/getting-started/installation/#windows";
}
public override string GetInstallationRecommendations()
{
return @"Windows Installation Recommendations:
1. Python: Install from Microsoft Store or python.org
- Microsoft Store: Search for 'Python 3.10' or higher
- Direct download: https://python.org/downloads/windows/
2. uv Package Manager: Install via PowerShell
- Run: powershell -ExecutionPolicy ByPass -c ""irm https://astral.sh/uv/install.ps1 | iex""
- Or download from: https://github.com/astral-sh/uv/releases
3. MCP Server: Will be installed automatically by MCP for Unity Bridge";
}
public override DependencyStatus DetectUv()
{
// First, honor overrides and cross-platform resolution via the base implementation
var status = base.DetectUv();
if (status.IsAvailable)
{
return status;
}
// If the user configured an override path but fallback was not used, keep the base result
// (failure typically means the override path is invalid and no system fallback found)
if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback)
{
return status;
}
try
{
string augmentedPath = BuildAugmentedPath();
// try to find uv
if (TryValidateUvWithPath("uv.exe", augmentedPath, out string uvVersion, out string uvPath))
{
status.IsAvailable = true;
status.Version = uvVersion;
status.Path = uvPath;
status.Details = $"Found uv {uvVersion} at {uvPath}";
return status;
}
// try to find uvx
if (TryValidateUvWithPath("uvx.exe", augmentedPath, out string uvxVersion, out string uvxPath))
{
status.IsAvailable = true;
status.Version = uvxVersion;
status.Path = uvxPath;
status.Details = $"Found uvx {uvxVersion} at {uvxPath} (fallback)";
return status;
}
status.ErrorMessage = "uv not found in PATH";
status.Details = "Install uv package manager and ensure it's added to PATH.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting uv: {ex.Message}";
}
return status;
}
private bool TryFindPythonViaUv(out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
string augmentedPath = BuildAugmentedPath();
// Try to list installed python versions via uvx
if (!ExecPath.TryRun("uv", "python list", null, out string stdout, out string stderr, 5000, augmentedPath))
return false;
var lines = stdout.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Contains("<download available>")) continue;
var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 2)
{
string potentialPath = parts[parts.Length - 1];
if (File.Exists(potentialPath) &&
(potentialPath.EndsWith("python.exe") || potentialPath.EndsWith("python3.exe")))
{
if (TryValidatePython(potentialPath, out version, out fullPath))
{
return true;
}
}
}
}
}
catch
{
// Ignore errors if uv is not installed or fails
}
return false;
}
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
string augmentedPath = BuildAugmentedPath();
// First, try to resolve the absolute path for better UI/logging display
string commandToRun = pythonPath;
if (TryFindInPath(pythonPath, out string resolvedPath))
{
commandToRun = resolvedPath;
}
// Run 'python --version' to get the version
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr, 5000, augmentedPath))
return false;
// Check stdout first, then stderr (some Python distributions output to stderr)
string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
if (output.StartsWith("Python "))
{
version = output.Substring(7);
fullPath = commandToRun;
if (TryParseVersion(version, out var major, out var minor))
{
return major > 3 || (major == 3 && minor >= 10);
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
protected override bool TryFindInPath(string executable, out string fullPath)
{
fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath());
return !string.IsNullOrEmpty(fullPath);
}
protected string BuildAugmentedPath()
{
var additions = GetPathAdditions();
if (additions.Length == 0) return null;
// Only return the additions - ExecPath.TryRun will prepend to existing PATH
return string.Join(Path.PathSeparator, additions);
}
private string[] GetPathAdditions()
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var additions = new List<string>();
// uv common installation paths
if (!string.IsNullOrEmpty(localAppData))
additions.Add(Path.Combine(localAppData, "Programs", "uv"));
if (!string.IsNullOrEmpty(programFiles))
additions.Add(Path.Combine(programFiles, "uv"));
// npm global paths
if (!string.IsNullOrEmpty(appData))
additions.Add(Path.Combine(appData, "npm"));
if (!string.IsNullOrEmpty(localAppData))
additions.Add(Path.Combine(localAppData, "npm"));
// Python common paths
if (!string.IsNullOrEmpty(localAppData))
additions.Add(Path.Combine(localAppData, "Programs", "Python"));
// Instead of hardcoded versions, enumerate existing directories
if (!string.IsNullOrEmpty(programFiles))
{
try
{
var pythonDirs = Directory.GetDirectories(programFiles, "Python3*")
.OrderByDescending(d => d); // Newest first
foreach (var dir in pythonDirs)
{
additions.Add(dir);
}
}
catch { /* Ignore if directory doesn't exist */ }
}
// User scripts
if (!string.IsNullOrEmpty(homeDir))
additions.Add(Path.Combine(homeDir, ".local", "bin"));
return additions.ToArray();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1aedc29caa5704c07b487d20a27e9334
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,93 @@
using System;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Dependencies
{
/// <summary>
/// Runs the official uv installer for the current platform. UI-agnostic: build the command,
/// run it (off the main thread), and report the outcome. The caller owns confirmation and
/// re-checking dependencies afterwards.
/// Installer reference: https://docs.astral.sh/uv/getting-started/installation/
/// </summary>
public static class UvInstaller
{
public readonly struct UvInstallResult
{
public readonly bool Success;
public readonly string Output;
public UvInstallResult(bool success, string output)
{
Success = success;
Output = output ?? string.Empty;
}
}
/// <summary>One-click install is available on Windows, macOS and Linux.</summary>
public static bool IsSupported =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ||
RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
/// <summary>
/// Build the installer invocation for the current platform. Pure and testable — the
/// returned command is exactly what <see cref="Run"/> executes and what the confirm
/// dialog shows the user.
/// </summary>
public static (string file, string arguments) BuildInstallCommand()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return ("powershell",
"-NoProfile -ExecutionPolicy ByPass -c \"irm https://astral.sh/uv/install.ps1 | iex\"");
}
// macOS and Linux share the POSIX shell installer.
return ("/bin/sh", "-c \"curl -LsSf https://astral.sh/uv/install.sh | sh\"");
}
/// <summary>Human-readable one-line description of the command that will run.</summary>
public static string DescribeCommand()
{
var (file, arguments) = BuildInstallCommand();
return $"{file} {arguments}";
}
/// <summary>
/// Run the installer and return the outcome. Blocks until the installer exits or the
/// timeout elapses, so call it off the main thread (e.g. via Task.Run).
/// </summary>
public static UvInstallResult Run(int timeoutMs = 180000)
{
try
{
var (file, arguments) = BuildInstallCommand();
bool ok = ExecPath.TryRun(file, arguments, null, out string stdout, out string stderr, timeoutMs);
return new UvInstallResult(ok, Combine(stdout, stderr));
}
catch (Exception ex)
{
return new UvInstallResult(false, ex.Message);
}
}
private static string Combine(string stdout, string stderr)
{
string outText = (stdout ?? string.Empty).Trim();
string errText = (stderr ?? string.Empty).Trim();
string combined =
string.IsNullOrEmpty(errText) ? outText :
string.IsNullOrEmpty(outText) ? errText :
outText + "\n" + errText;
// Keep dialogs readable — echo only the tail of long installer output.
const int max = 1500;
if (combined.Length > max)
{
combined = "…" + combined.Substring(combined.Length - max);
}
return combined;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 55a5a123f9454b75906bdb5b7d008990
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c11944bcfb9ec4576bab52874b7df584
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ea652131dcdaa44ca8cb35cd1191be3f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 94cb070dc5e15024da86150b27699ca0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,90 @@
using System;
using System.IO;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Project-path conversions shared by the asset-gen import/write code: project-relative
/// ("Assets/...") ↔ absolute on-disk paths, with forward-slash normalization for
/// cross-platform consistency.
/// </summary>
public static class AssetGenPaths
{
/// <summary>Resolve a project-relative ("Assets/...") path to an absolute, forward-slashed path.</summary>
public static string ToAbsolute(string projectRelative)
{
if (string.IsNullOrWhiteSpace(projectRelative)) return projectRelative;
string p = projectRelative.Replace('\\', '/');
string abs = Path.IsPathRooted(p) ? p : Path.Combine(ProjectRoot(), p);
return Path.GetFullPath(abs).Replace('\\', '/');
}
/// <summary>Convert an absolute (or already-relative) path to a project-relative ("Assets/...") path.</summary>
public static string ToProjectRelative(string path)
{
if (TryGetAssetsRelativePath(path, out string rel)) return rel;
return path?.Replace('\\', '/');
}
/// <summary>
/// Normalize an absolute or "Assets/..." path and verify it resolves inside the project's
/// Assets directory. Rejects traversal such as "Assets/../ProjectSettings".
/// </summary>
public static bool TryGetAssetsRelativePath(string path, out string projectRelative)
{
projectRelative = null;
if (string.IsNullOrWhiteSpace(path)) return false;
try
{
string p = path.Replace('\\', '/');
string abs;
if (p == "Assets" || p.StartsWith("Assets/", StringComparison.Ordinal))
{
abs = Path.Combine(ProjectRoot(), p);
}
else if (Path.IsPathRooted(p))
{
abs = p;
}
else
{
return false;
}
string full = Path.GetFullPath(abs).Replace('\\', '/');
string dataPath = Path.GetFullPath(Application.dataPath).Replace('\\', '/').TrimEnd('/');
if (string.Equals(full, dataPath, StringComparison.Ordinal))
{
projectRelative = "Assets";
return true;
}
string prefix = dataPath + "/";
if (!full.StartsWith(prefix, StringComparison.Ordinal)) return false;
projectRelative = "Assets/" + full.Substring(prefix.Length);
return true;
}
catch
{
return false;
}
}
/// <summary>Normalize an Assets folder path, trimming trailing slashes.</summary>
public static bool TryGetAssetsFolder(string path, out string projectRelative)
{
if (!TryGetAssetsRelativePath(path, out projectRelative)) return false;
projectRelative = projectRelative.TrimEnd('/');
return true;
}
private static string ProjectRoot()
{
string dataPath = Path.GetFullPath(Application.dataPath).Replace('\\', '/');
return dataPath.Substring(0, dataPath.Length - "Assets".Length);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: afc641959f464b55911f6703a96b5f37
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,73 @@
using MCPForUnity.Editor.Constants;
using UnityEditor;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Per-user, NON-SECRET configuration for the AI Asset Generation feature.
///
/// Provider API keys are never stored here — they live in the OS secure store
/// (<see cref="MCPForUnity.Editor.Security.SecureKeyStore"/>). Only UI/behavior
/// preferences (selected provider, default format, output folder, normalize toggle,
/// per-provider enable flags) are kept in EditorPrefs.
/// </summary>
public static class AssetGenPrefs
{
public const string DefaultOutputRoot = "Assets/Generated";
public const string DefaultModelProvider = "tripo";
public const string DefaultImageProvider = "fal";
public const string DefaultFormatValue = "glb";
public static string ModelProvider
{
get => EditorPrefs.GetString(EditorPrefKeys.AssetGenSelectedModelProvider, DefaultModelProvider);
set => SetOrDelete(EditorPrefKeys.AssetGenSelectedModelProvider, value);
}
public static string ImageProvider
{
get => EditorPrefs.GetString(EditorPrefKeys.AssetGenSelectedImageProvider, DefaultImageProvider);
set => SetOrDelete(EditorPrefKeys.AssetGenSelectedImageProvider, value);
}
public static string DefaultFormat
{
get => EditorPrefs.GetString(EditorPrefKeys.AssetGenDefaultFormat, DefaultFormatValue);
set => SetOrDelete(EditorPrefKeys.AssetGenDefaultFormat, value);
}
/// <summary>Project-relative root under which generated assets are written. Defaults to Assets/Generated.</summary>
public static string OutputRoot
{
get
{
string v = EditorPrefs.GetString(EditorPrefKeys.AssetGenOutputRoot, string.Empty);
return string.IsNullOrWhiteSpace(v) ? DefaultOutputRoot : v;
}
set => SetOrDelete(EditorPrefKeys.AssetGenOutputRoot, value);
}
/// <summary>When true, imported models are uniformly scaled to a target size on import.</summary>
public static bool AutoNormalize
{
get => EditorPrefs.GetBool(EditorPrefKeys.AssetGenAutoNormalize, true);
set => EditorPrefs.SetBool(EditorPrefKeys.AssetGenAutoNormalize, value);
}
public static bool IsProviderEnabled(string providerId) =>
!string.IsNullOrEmpty(providerId)
&& EditorPrefs.GetBool(EditorPrefKeys.AssetGenProviderEnabledPrefix + providerId, false);
public static void SetProviderEnabled(string providerId, bool enabled)
{
if (string.IsNullOrEmpty(providerId)) return;
EditorPrefs.SetBool(EditorPrefKeys.AssetGenProviderEnabledPrefix + providerId, enabled);
}
private static void SetOrDelete(string key, string value)
{
if (string.IsNullOrWhiteSpace(value)) EditorPrefs.DeleteKey(key);
else EditorPrefs.SetString(key, value.Trim());
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7704064abf1412e890a066628bae30a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,668 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Services;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Provides common utility methods for working with Unity asset paths.
/// </summary>
public static class AssetPathUtility
{
/// <summary>
/// Normalizes path separators to forward slashes without modifying the path structure.
/// Use this for non-asset paths (e.g., file system paths, relative directories).
/// </summary>
public static string NormalizeSeparators(string path)
{
if (string.IsNullOrEmpty(path))
return path;
return path.Replace('\\', '/');
}
/// <summary>
/// Normalizes a Unity asset path by ensuring forward slashes are used and that it is rooted under "Assets/".
/// Also protects against path traversal attacks using "../" sequences.
/// </summary>
public static string SanitizeAssetPath(string path)
{
if (string.IsNullOrEmpty(path))
{
return path;
}
path = NormalizeSeparators(path);
// Check for path traversal sequences
if (path.Contains(".."))
{
McpLog.Warn($"[AssetPathUtility] Path contains potential traversal sequence: '{path}'");
return null;
}
// Ensure path starts with Assets/
if (string.Equals(path, "Assets", StringComparison.OrdinalIgnoreCase))
{
return "Assets";
}
if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
return "Assets/" + path.TrimStart('/');
}
return path;
}
/// <summary>
/// Checks if a given asset path is valid and safe (no traversal, within Assets folder).
/// </summary>
/// <returns>True if the path is valid, false otherwise.</returns>
public static bool IsValidAssetPath(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
// Normalize for comparison
string normalized = NormalizeSeparators(path);
// Must start with Assets/
if (!normalized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Must not contain traversal sequences
if (normalized.Contains(".."))
{
return false;
}
// Must not contain invalid path characters
char[] invalidChars = { ':', '*', '?', '"', '<', '>', '|' };
foreach (char c in invalidChars)
{
if (normalized.IndexOf(c) >= 0)
{
return false;
}
}
return true;
}
/// <summary>
/// Gets the MCP for Unity package root path.
/// Works for registry Package Manager, local Package Manager, and Asset Store installations.
/// </summary>
/// <returns>The package root path (virtual for PM, absolute for Asset Store), or null if not found</returns>
public static string GetMcpPackageRootPath()
{
try
{
// Try Package Manager first (registry and local installs)
var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly);
if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.assetPath))
{
return packageInfo.assetPath;
}
// Fallback to AssetDatabase for Asset Store installs (Assets/MCPForUnity)
string[] guids = AssetDatabase.FindAssets($"t:Script {nameof(AssetPathUtility)}");
if (guids.Length == 0)
{
McpLog.Warn("Could not find AssetPathUtility script in AssetDatabase");
return null;
}
string scriptPath = AssetDatabase.GUIDToAssetPath(guids[0]);
// Script is at: {packageRoot}/Editor/Helpers/AssetPathUtility.cs
// Extract {packageRoot}
int editorIndex = scriptPath.LastIndexOf("/Editor/", StringComparison.Ordinal);
if (editorIndex >= 0)
{
return scriptPath.Substring(0, editorIndex);
}
McpLog.Warn($"Could not determine package root from script path: {scriptPath}");
return null;
}
catch (Exception ex)
{
McpLog.Error($"Failed to get package root path: {ex.Message}");
return null;
}
}
/// <summary>
/// Reads and parses the package.json file for MCP for Unity.
/// Handles both Package Manager (registry/local) and Asset Store installations.
/// </summary>
/// <returns>JObject containing package.json data, or null if not found or parse failed</returns>
public static JObject GetPackageJson()
{
try
{
string packageRoot = GetMcpPackageRootPath();
if (string.IsNullOrEmpty(packageRoot))
{
return null;
}
string packageJsonPath = Path.Combine(packageRoot, "package.json");
// Convert virtual asset path to file system path
if (packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase))
{
// Package Manager install - must use PackageInfo.resolvedPath
// Virtual paths like "Packages/..." don't work with File.Exists()
// Registry packages live in Library/PackageCache/package@version/
var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly);
if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.resolvedPath))
{
packageJsonPath = Path.Combine(packageInfo.resolvedPath, "package.json");
}
else
{
McpLog.Warn("Could not resolve Package Manager path for package.json");
return null;
}
}
else if (packageRoot.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
// Asset Store install - convert to absolute file system path
// Application.dataPath is the absolute path to the Assets folder
string relativePath = packageRoot.Substring("Assets/".Length);
packageJsonPath = Path.Combine(Application.dataPath, relativePath, "package.json");
}
if (!File.Exists(packageJsonPath))
{
McpLog.Warn($"package.json not found at: {packageJsonPath}");
return null;
}
string json = File.ReadAllText(packageJsonPath);
return JObject.Parse(json);
}
catch (Exception ex)
{
McpLog.Warn($"Failed to read or parse package.json: {ex.Message}");
return null;
}
}
/// <summary>
/// Gets the package source for the MCP server (used with uvx --from).
/// Checks for EditorPrefs override first (supports git URLs, file:// paths, etc.),
/// then falls back to PyPI package reference.
/// When the override is a local path, auto-corrects to the "Server" subdirectory
/// if the path doesn't contain pyproject.toml but Server/pyproject.toml exists.
/// </summary>
/// <returns>Package source string for uvx --from argument</returns>
public static string GetMcpServerPackageSource()
{
// Check for override first (supports git URLs, file:// paths, local paths)
string sourceOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
if (!string.IsNullOrEmpty(sourceOverride))
{
string resolved = ResolveLocalServerPath(sourceOverride);
// Persist the corrected path so future reads are consistent
if (resolved != sourceOverride)
{
EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, resolved);
McpLog.Info($"Auto-corrected server source override from '{sourceOverride}' to '{resolved}'");
}
return resolved;
}
// Default to PyPI package (avoids Windows long path issues with git clone)
string version = GetPackageVersion();
if (version == "unknown")
{
// Fall back to latest PyPI version so configs remain valid in test scenarios
return "mcpforunityserver";
}
// Package.json uses semver prerelease tags (e.g., 9.4.5-beta.1) that are not valid
// PEP 440 pins for uvx. Use the beta prerelease range instead of a pinned prerelease.
if (IsSemVerPreRelease(version))
{
return "mcpforunityserver>=0.0.0a0";
}
return $"mcpforunityserver=={version}";
}
/// <summary>
/// Validates and auto-corrects a local server source path to ensure it points to the
/// directory containing pyproject.toml. If the path points to a parent directory
/// (e.g. the repo root "unity-mcp") instead of the Python package directory ("Server"),
/// this checks for a "Server" subdirectory with pyproject.toml and returns that path.
/// Non-local paths (URLs, PyPI references) are returned unchanged.
/// </summary>
internal static string ResolveLocalServerPath(string path)
{
if (string.IsNullOrEmpty(path))
return path;
// Skip non-local paths (git URLs, PyPI package names, etc.)
if (path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("git+", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("ssh://", StringComparison.OrdinalIgnoreCase))
{
return path;
}
// If it looks like a PyPI package reference (no path separators), skip
if (!path.Contains('/') && !path.Contains('\\') && !path.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
{
return path;
}
// Strip file:// prefix for filesystem checks, preserve for return value
string checkPath = path;
string prefix = string.Empty;
if (checkPath.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
prefix = checkPath.Substring(0, 7); // preserve original casing
checkPath = checkPath.Substring(7);
}
// Already correct — pyproject.toml exists at this path
if (System.IO.File.Exists(System.IO.Path.Combine(checkPath, "pyproject.toml")))
{
return path;
}
// Check if "Server" subdirectory contains pyproject.toml
string serverSubDir = System.IO.Path.Combine(checkPath, "Server");
if (System.IO.File.Exists(System.IO.Path.Combine(serverSubDir, "pyproject.toml")))
{
return prefix + serverSubDir;
}
// Return as-is; uvx will report the error if the path is truly invalid
return path;
}
/// <summary>
/// Deprecated: Use GetMcpServerPackageSource() instead.
/// Kept for backwards compatibility.
/// </summary>
[System.Obsolete("Use GetMcpServerPackageSource() instead")]
public static string GetMcpServerGitUrl() => GetMcpServerPackageSource();
/// <summary>
/// Gets structured uvx command parts for different client configurations
/// </summary>
/// <returns>Tuple containing (uvxPath, fromUrl, packageName)</returns>
public static (string uvxPath, string fromUrl, string packageName) GetUvxCommandParts()
{
string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
string fromUrl = GetMcpServerPackageSource();
string packageName = "mcp-for-unity";
return (uvxPath, fromUrl, packageName);
}
/// <summary>
/// Builds the uvx package source arguments for the MCP server.
/// Handles prerelease package mode (prerelease from PyPI) vs stable mode (pinned version or override).
/// Centralizes the prerelease logic to avoid duplication between HTTP and stdio transports.
/// Priority: explicit fromUrl override > package-version-driven prerelease mode > stable pinned package.
/// NOTE: This overload reads from EditorPrefs/cache and MUST be called from the main thread.
/// For background threads, use the overload that accepts pre-captured parameters.
/// </summary>
/// <param name="quoteFromPath">Whether to quote the --from path (needed for command-line strings, not for arg lists)</param>
/// <returns>The package source arguments (e.g., "--prerelease explicit --from mcpforunityserver>=0.0.0a0")</returns>
public static string GetBetaServerFromArgs(bool quoteFromPath = false)
{
string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
string packageSource = GetMcpServerPackageSource();
return GetBetaServerFromArgs(gitUrlOverride, packageSource, quoteFromPath);
}
/// <summary>
/// Thread-safe overload that accepts pre-captured values.
/// Use this when calling from background threads.
/// </summary>
/// <param name="gitUrlOverride">Pre-captured value from EditorPrefs GitUrlOverride</param>
/// <param name="packageSource">Pre-captured value from GetMcpServerPackageSource()</param>
/// <param name="quoteFromPath">Whether to quote the --from path</param>
public static string GetBetaServerFromArgs(string gitUrlOverride, string packageSource, bool quoteFromPath = false)
{
// Explicit override (local path, git URL, etc.) always wins
if (!string.IsNullOrEmpty(gitUrlOverride))
{
string fromValue = quoteFromPath ? $"\"{gitUrlOverride}\"" : gitUrlOverride;
return $"--from {fromValue}";
}
bool usePrereleaseRange = string.Equals(packageSource, "mcpforunityserver>=0.0.0a0", StringComparison.OrdinalIgnoreCase);
// Prerelease package mode: use prerelease from PyPI.
if (usePrereleaseRange)
{
// Use --prerelease explicit with version specifier to only get prereleases of our package,
// not of dependencies (which can be broken on PyPI).
string fromValue = quoteFromPath ? "\"mcpforunityserver>=0.0.0a0\"" : "mcpforunityserver>=0.0.0a0";
return $"--prerelease explicit --from {fromValue}";
}
// Standard mode: use pinned version from package.json
if (!string.IsNullOrEmpty(packageSource))
{
string fromValue = quoteFromPath ? $"\"{packageSource}\"" : packageSource;
return $"--from {fromValue}";
}
return string.Empty;
}
/// <summary>
/// Builds the uvx package source arguments as a list (for JSON config builders).
/// Priority: explicit fromUrl override > package-version-driven prerelease mode > stable pinned package.
/// NOTE: This overload reads from EditorPrefs/cache and MUST be called from the main thread.
/// For background threads, use the overload that accepts pre-captured parameters.
/// </summary>
/// <returns>List of arguments to add to uvx command</returns>
public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList()
{
string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
string packageSource = GetMcpServerPackageSource();
return GetBetaServerFromArgsList(gitUrlOverride, packageSource);
}
/// <summary>
/// Thread-safe overload that accepts pre-captured values.
/// Use this when calling from background threads.
/// </summary>
/// <param name="gitUrlOverride">Pre-captured value from EditorPrefs GitUrlOverride</param>
/// <param name="packageSource">Pre-captured value from GetMcpServerPackageSource()</param>
public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList(string gitUrlOverride, string packageSource)
{
var args = new System.Collections.Generic.List<string>();
// Explicit override (local path, git URL, etc.) always wins
if (!string.IsNullOrEmpty(gitUrlOverride))
{
args.Add("--from");
args.Add(gitUrlOverride);
return args;
}
bool usePrereleaseRange = string.Equals(packageSource, "mcpforunityserver>=0.0.0a0", StringComparison.OrdinalIgnoreCase);
// Prerelease package mode: use prerelease from PyPI.
if (usePrereleaseRange)
{
args.Add("--prerelease");
args.Add("explicit");
args.Add("--from");
args.Add("mcpforunityserver>=0.0.0a0");
return args;
}
// Standard mode: use pinned version from package.json
if (!string.IsNullOrEmpty(packageSource))
{
args.Add("--from");
args.Add(packageSource);
}
return args;
}
/// <summary>
/// Determines whether uvx should use --no-cache --refresh flags.
/// Returns true if DevModeForceServerRefresh is enabled OR if the server URL is a local path.
/// Local paths (file:// or absolute) always need fresh builds to avoid stale uvx cache.
/// Note: --reinstall is not supported by uvx and will cause a warning.
/// </summary>
public static bool ShouldForceUvxRefresh()
{
bool devForceRefresh = false;
try { devForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); } catch { }
if (devForceRefresh)
return true;
// Auto-enable force refresh when using a local path override.
return IsLocalServerPath();
}
private static bool _offlineCacheResult;
private static double _offlineCacheTimestamp = -999;
private const double OfflineCacheTtlSeconds = 30.0;
/// <summary>
/// Determines whether uvx should use --offline mode for faster startup.
/// Runs a lightweight probe (uvx --offline ... mcp-for-unity --help) with a 3-second timeout
/// to check if the package is already cached. If cached, --offline skips the network
/// dependency check that can hang for 30+ seconds on poor connections.
/// Returns false if force refresh is enabled (new download needed).
/// The result is cached for 30 seconds to avoid redundant subprocess spawns.
/// Must be called on the main thread (reads EditorPrefs).
/// </summary>
public static bool ShouldUseUvxOffline()
{
if (ShouldForceUvxRefresh())
return false;
return GetCachedOfflineProbeResult();
}
private static bool GetCachedOfflineProbeResult()
{
double now = EditorApplication.timeSinceStartup;
if (now - _offlineCacheTimestamp < OfflineCacheTtlSeconds)
return _offlineCacheResult;
bool result = RunOfflineProbe();
_offlineCacheResult = result;
_offlineCacheTimestamp = now;
return result;
}
private static bool RunOfflineProbe()
{
try
{
string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
if (string.IsNullOrEmpty(uvxPath))
return false;
string fromArgs = GetBetaServerFromArgs(quoteFromPath: false);
string probeArgs = string.IsNullOrEmpty(fromArgs)
? "--offline mcp-for-unity --help"
: $"--offline {fromArgs} mcp-for-unity --help";
return ExecPath.TryRun(uvxPath, probeArgs, null, out _, out _, timeoutMs: 3000);
}
catch
{
return false;
}
}
/// <summary>
/// Returns the uvx dev-mode flags as a single string for command-line builders.
/// Returns "--no-cache --refresh " if force refresh is enabled,
/// "--offline " if the cache is warm, or string.Empty otherwise.
/// Must be called on the main thread (reads EditorPrefs).
/// </summary>
public static string GetUvxDevFlags()
{
bool forceRefresh = ShouldForceUvxRefresh();
return GetUvxDevFlags(forceRefresh, !forceRefresh && GetCachedOfflineProbeResult());
}
/// <summary>
/// Returns the uvx dev-mode flags from pre-captured bool values.
/// Use this overload when values were captured on the main thread for background use.
/// </summary>
public static string GetUvxDevFlags(bool forceRefresh, bool useOffline)
{
if (forceRefresh) return "--no-cache --refresh ";
if (useOffline) return "--offline ";
return string.Empty;
}
/// <summary>
/// Returns the uvx dev-mode flags as a list of individual arguments.
/// Suitable for callers that build argument lists (ConfigJsonBuilder, CodexConfigHelper).
/// Must be called on the main thread (reads EditorPrefs).
/// </summary>
public static IReadOnlyList<string> GetUvxDevFlagsList()
{
bool forceRefresh = ShouldForceUvxRefresh();
if (forceRefresh) return new[] { "--no-cache", "--refresh" };
if (GetCachedOfflineProbeResult()) return new[] { "--offline" };
return Array.Empty<string>();
}
/// <summary>
/// Returns true if the server URL is a local path (file:// or absolute path).
/// </summary>
public static bool IsLocalServerPath()
{
string fromUrl = GetMcpServerPackageSource();
if (string.IsNullOrEmpty(fromUrl))
return false;
// Check for file:// protocol or absolute local path
if (fromUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
return true;
try
{
return System.IO.Path.IsPathRooted(fromUrl);
}
catch (System.ArgumentException)
{
// fromUrl contains characters illegal in paths (e.g. a remote URL)
return false;
}
}
/// <summary>
/// Gets the local server path if GitUrlOverride points to a local directory.
/// Returns null if not using a local path.
/// </summary>
public static string GetLocalServerPath()
{
if (!IsLocalServerPath())
return null;
string fromUrl = GetMcpServerPackageSource();
if (fromUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
// Strip file:// prefix
fromUrl = fromUrl.Substring(7);
}
return fromUrl;
}
/// <summary>
/// Cleans stale Python build artifacts from the local server path.
/// This is necessary because Python's build system doesn't remove deleted files from build/,
/// and the auto-discovery mechanism will pick up old .py files causing ghost resources/tools.
/// </summary>
/// <returns>True if cleaning was performed, false if not applicable or failed.</returns>
public static bool CleanLocalServerBuildArtifacts()
{
string localPath = GetLocalServerPath();
if (string.IsNullOrEmpty(localPath))
return false;
// Clean the build/ directory which can contain stale .py files
string buildPath = System.IO.Path.Combine(localPath, "build");
if (System.IO.Directory.Exists(buildPath))
{
try
{
System.IO.Directory.Delete(buildPath, recursive: true);
McpLog.Info($"Cleaned stale build artifacts from: {buildPath}");
return true;
}
catch (Exception ex)
{
McpLog.Warn($"Failed to clean build artifacts: {ex.Message}");
return false;
}
}
return false;
}
/// <summary>
/// Gets the package version from package.json
/// </summary>
/// <returns>Version string, or "unknown" if not found</returns>
public static string GetPackageVersion()
{
try
{
var packageJson = GetPackageJson();
if (packageJson == null)
{
return "unknown";
}
string version = packageJson["version"]?.ToString();
return string.IsNullOrEmpty(version) ? "unknown" : version;
}
catch (Exception ex)
{
McpLog.Warn($"Failed to get package version: {ex.Message}");
return "unknown";
}
}
/// <summary>
/// Returns true if the installed package version is a prerelease (beta, alpha, rc, etc.).
/// Used to auto-enable beta server mode for beta package users.
/// </summary>
public static bool IsPreReleaseVersion()
{
try
{
string version = GetPackageVersion();
if (string.IsNullOrEmpty(version) || version == "unknown")
return false;
return IsSemVerPreRelease(version);
}
catch
{
return false;
}
}
private static bool IsSemVerPreRelease(string version)
{
if (string.IsNullOrEmpty(version))
return false;
// Common semver prerelease indicators:
// e.g., "9.3.0-beta.1", "9.3.0-alpha", "9.3.0-rc.2", "9.3.0-preview"
return version.Contains("-beta", StringComparison.OrdinalIgnoreCase) ||
version.Contains("-alpha", StringComparison.OrdinalIgnoreCase) ||
version.Contains("-rc", StringComparison.OrdinalIgnoreCase) ||
version.Contains("-preview", StringComparison.OrdinalIgnoreCase) ||
version.Contains("-pre", StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d42f5b5ea5d4d43ad1a771e14bda2a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Best-effort detection of a locally installed Blender application, for the Asset Gen tab's
/// "Blender → Unity handoff" hint. This finds the Blender APP only — it cannot tell whether the
/// BlenderMCP server is configured in the user's AI client (that lives outside Unity).
/// </summary>
internal static class BlenderDetection
{
/// <summary>True if a Blender executable is found in a well-known location or on PATH.</summary>
public static bool IsInstalled()
{
try { return DetectIn(CandidatePaths(), File.Exists); }
catch { return false; }
}
/// <summary>Pure core: true if <paramref name="exists"/> reports any candidate present. Testable.</summary>
internal static bool DetectIn(IEnumerable<string> candidates, Func<string, bool> exists)
{
if (candidates == null || exists == null) return false;
foreach (string c in candidates)
if (!string.IsNullOrEmpty(c) && exists(c)) return true;
return false;
}
/// <summary>Well-known Blender executable paths for the current platform, plus PATH entries.</summary>
internal static IEnumerable<string> CandidatePaths()
{
var list = new List<string>();
bool win = Application.platform == RuntimePlatform.WindowsEditor;
string exeName = win ? "blender.exe" : "blender";
// PATH entries: <dir>/blender(.exe)
string pathVar = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
foreach (string dir in pathVar.Split(win ? ';' : ':'))
if (!string.IsNullOrWhiteSpace(dir)) list.Add(Path.Combine(dir.Trim(), exeName));
switch (Application.platform)
{
case RuntimePlatform.OSXEditor:
list.Add("/Applications/Blender.app/Contents/MacOS/Blender");
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (!string.IsNullOrEmpty(home))
list.Add(Path.Combine(home, "Applications/Blender.app/Contents/MacOS/Blender"));
break;
case RuntimePlatform.WindowsEditor:
foreach (string pf in new[]
{
Environment.GetEnvironmentVariable("ProgramFiles"),
Environment.GetEnvironmentVariable("ProgramFiles(x86)")
})
{
if (string.IsNullOrEmpty(pf)) continue;
string foundation = Path.Combine(pf, "Blender Foundation");
// Blender installs under a version subdir (Blender X.Y); enumerate them.
try
{
if (Directory.Exists(foundation))
foreach (string d in Directory.GetDirectories(foundation))
list.Add(Path.Combine(d, "blender.exe"));
}
catch { /* unreadable dir; ignore */ }
}
break;
case RuntimePlatform.LinuxEditor:
list.Add("/usr/bin/blender");
list.Add("/usr/local/bin/blender");
list.Add("/snap/bin/blender");
list.Add("/var/lib/flatpak/exports/bin/org.blender.Blender");
break;
}
return list;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bf9f02bd846b4e8db34827d949046f70
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,316 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Services;
using MCPForUnity.External.Tommy;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Codex CLI specific configuration helpers. Handles TOML snippet
/// generation and lightweight parsing so Codex can join the auto-setup
/// flow alongside JSON-based clients.
/// </summary>
public static class CodexConfigHelper
{
private static void AddUvxModeFlags(TomlArray args)
{
if (args == null) return;
foreach (var flag in AssetPathUtility.GetUvxDevFlagsList())
args.Add(new TomlString { Value = flag });
}
public static string BuildCodexServerBlock(string uvPath)
{
var table = new TomlTable();
var mcpServers = new TomlTable();
var unityMCP = new TomlTable();
// Check transport preference
bool useHttpTransport = EditorPrefs.GetBool(MCPForUnity.Editor.Constants.EditorPrefKeys.UseHttpTransport, true);
if (useHttpTransport)
{
// HTTP mode: Use url field
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
unityMCP["url"] = new TomlString { Value = httpUrl };
// Enable Codex's Rust MCP client for HTTP/SSE transport
EnsureRmcpClientFeature(table);
}
else
{
// Stdio mode: Use command and args
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
unityMCP["command"] = uvxPath;
var args = new TomlArray();
AddUvxModeFlags(args);
// Use centralized helper for beta server / prerelease args
foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList())
{
args.Add(new TomlString { Value = arg });
}
args.Add(new TomlString { Value = packageName });
args.Add(new TomlString { Value = "--transport" });
args.Add(new TomlString { Value = "stdio" });
unityMCP["args"] = args;
// Add Windows-specific environment configuration for stdio mode
var platformService = MCPServiceLocator.Platform;
if (platformService.IsWindows())
{
var envTable = new TomlTable { IsInline = true };
envTable["SystemRoot"] = new TomlString { Value = platformService.GetSystemRoot() };
unityMCP["env"] = envTable;
}
// Allow extra time for uvx to download packages on first run
unityMCP["startup_timeout_sec"] = new TomlInteger { Value = 60 };
}
mcpServers["unityMCP"] = unityMCP;
table["mcp_servers"] = mcpServers;
using var writer = new StringWriter();
table.WriteTo(writer);
return writer.ToString();
}
public static string UpsertCodexServerBlock(string existingToml, string uvPath)
{
// Parse existing TOML or create new root table
var root = TryParseToml(existingToml) ?? new TomlTable();
bool useHttpTransport = EditorPrefs.GetBool(MCPForUnity.Editor.Constants.EditorPrefKeys.UseHttpTransport, true);
// Ensure mcp_servers table exists
if (!root.TryGetNode("mcp_servers", out var mcpServersNode) || !(mcpServersNode is TomlTable))
{
root["mcp_servers"] = new TomlTable();
}
var mcpServers = root["mcp_servers"] as TomlTable;
// Create or update unityMCP table
mcpServers["unityMCP"] = CreateUnityMcpTable(uvPath);
if (useHttpTransport)
{
EnsureRmcpClientFeature(root);
}
// Serialize back to TOML
using var writer = new StringWriter();
root.WriteTo(writer);
return writer.ToString();
}
public static bool TryParseCodexServer(string toml, out string command, out string[] args)
{
return TryParseCodexServer(toml, out command, out args, out _);
}
public static bool TryParseCodexServer(string toml, out string command, out string[] args, out string url)
{
command = null;
args = null;
url = null;
var root = TryParseToml(toml);
if (root == null) return false;
if (!TryGetTable(root, "mcp_servers", out var servers)
&& !TryGetTable(root, "mcpServers", out servers))
{
return false;
}
if (!TryGetTable(servers, "unityMCP", out var unity))
{
return false;
}
// Check for HTTP mode (url field)
url = GetTomlString(unity, "url");
if (!string.IsNullOrEmpty(url))
{
// HTTP mode detected - return true with url
return true;
}
// Check for stdio mode (command + args)
command = GetTomlString(unity, "command");
args = GetTomlStringArray(unity, "args");
return !string.IsNullOrEmpty(command) && args != null;
}
/// <summary>
/// Safely parses TOML string, returning null on failure
/// </summary>
private static TomlTable TryParseToml(string toml)
{
if (string.IsNullOrWhiteSpace(toml)) return null;
try
{
using var reader = new StringReader(toml);
return TOML.Parse(reader);
}
catch (TomlParseException)
{
return null;
}
catch (TomlSyntaxException)
{
return null;
}
catch (FormatException)
{
return null;
}
}
/// <summary>
/// Creates a TomlTable for the unityMCP server configuration
/// </summary>
/// <param name="uvPath">Path to uv executable (used as fallback if uvx is not available)</param>
private static TomlTable CreateUnityMcpTable(string uvPath)
{
var unityMCP = new TomlTable();
// Check transport preference
bool useHttpTransport = EditorPrefs.GetBool(MCPForUnity.Editor.Constants.EditorPrefKeys.UseHttpTransport, true);
if (useHttpTransport)
{
// HTTP mode: Use url field
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
unityMCP["url"] = new TomlString { Value = httpUrl };
}
else
{
// Stdio mode: Use command and args
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
unityMCP["command"] = new TomlString { Value = uvxPath };
var argsArray = new TomlArray();
AddUvxModeFlags(argsArray);
// Use centralized helper for beta server / prerelease args
foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList())
{
argsArray.Add(new TomlString { Value = arg });
}
argsArray.Add(new TomlString { Value = packageName });
argsArray.Add(new TomlString { Value = "--transport" });
argsArray.Add(new TomlString { Value = "stdio" });
unityMCP["args"] = argsArray;
// Add Windows-specific environment configuration for stdio mode
var platformService = MCPServiceLocator.Platform;
if (platformService.IsWindows())
{
var envTable = new TomlTable { IsInline = true };
envTable["SystemRoot"] = new TomlString { Value = platformService.GetSystemRoot() };
unityMCP["env"] = envTable;
}
// Allow extra time for uvx to download packages on first run
unityMCP["startup_timeout_sec"] = new TomlInteger { Value = 60 };
}
return unityMCP;
}
/// <summary>
/// Ensures the features table contains the rmcp_client flag for HTTP/SSE transport.
/// </summary>
private static void EnsureRmcpClientFeature(TomlTable root)
{
if (root == null) return;
if (!root.TryGetNode("features", out var featuresNode) || featuresNode is not TomlTable features)
{
features = new TomlTable();
root["features"] = features;
}
features["rmcp_client"] = new TomlBoolean { Value = true };
}
private static bool TryGetTable(TomlTable parent, string key, out TomlTable table)
{
table = null;
if (parent == null) return false;
if (parent.TryGetNode(key, out var node))
{
if (node is TomlTable tbl)
{
table = tbl;
return true;
}
if (node is TomlArray array)
{
var firstTable = array.Children.OfType<TomlTable>().FirstOrDefault();
if (firstTable != null)
{
table = firstTable;
return true;
}
}
}
return false;
}
private static string GetTomlString(TomlTable table, string key)
{
if (table != null && table.TryGetNode(key, out var node))
{
if (node is TomlString str) return str.Value;
if (node.HasValue) return node.ToString();
}
return null;
}
private static string[] GetTomlStringArray(TomlTable table, string key)
{
if (table == null) return null;
if (!table.TryGetNode(key, out var node)) return null;
if (node is TomlArray array)
{
List<string> values = new List<string>();
foreach (TomlNode element in array.Children)
{
if (element is TomlString str)
{
values.Add(str.Value);
}
else if (element.HasValue)
{
values.Add(element.ToString());
}
}
return values.Count > 0 ? values.ToArray() : Array.Empty<string>();
}
if (node is TomlString single)
{
return new[] { single.Value };
}
return null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3e68082ffc0b4cd39d3747673a4cc22
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+997
View File
@@ -0,0 +1,997 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.Events;
using MCPForUnity.Runtime.Helpers;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Low-level component operations extracted from ManageGameObject and ManageComponents.
/// Provides pure C# operations without JSON parsing or response formatting.
/// </summary>
public static class ComponentOps
{
/// <summary>
/// Adds a component to a GameObject with Undo support.
/// </summary>
/// <param name="target">The target GameObject</param>
/// <param name="componentType">The type of component to add</param>
/// <param name="error">Error message if operation fails</param>
/// <returns>The added component, or null if failed</returns>
public static Component AddComponent(GameObject target, Type componentType, out string error)
{
error = null;
if (target == null)
{
error = "Target GameObject is null.";
return null;
}
if (componentType == null || !typeof(Component).IsAssignableFrom(componentType))
{
error = $"Type '{componentType?.Name ?? "null"}' is not a valid Component type.";
return null;
}
// Prevent adding duplicate Transform
if (componentType == typeof(Transform))
{
error = "Cannot add another Transform component.";
return null;
}
// Check for 2D/3D physics conflicts
string conflictError = CheckPhysicsConflict(target, componentType);
if (conflictError != null)
{
error = conflictError;
return null;
}
// Produce a clearer error when this component already exists and cannot be duplicated.
Component existingComponent = target.GetComponent(componentType);
if (existingComponent != null && !AllowsMultiple(target, componentType))
{
error = $"Component '{componentType.Name}' already exists on '{target.name}' and this type does not allow multiple instances.";
return null;
}
try
{
Component newComponent = Undo.AddComponent(target, componentType);
if (newComponent == null)
{
if (target.GetComponent(componentType) != null && !AllowsMultiple(target, componentType))
{
error = $"Component '{componentType.Name}' already exists on '{target.name}' and this type does not allow multiple instances.";
}
else
{
error = $"Failed to add component '{componentType.Name}' to '{target.name}'. Unity may restrict this component on the current target.";
}
return null;
}
// Apply default values for specific component types
ApplyDefaultValues(newComponent);
return newComponent;
}
catch (Exception ex)
{
error = $"Error adding component '{componentType.Name}': {ex.Message}";
return null;
}
}
/// <summary>
/// Removes a component from a GameObject with Undo support.
/// </summary>
/// <param name="target">The target GameObject</param>
/// <param name="componentType">The type of component to remove</param>
/// <param name="error">Error message if operation fails</param>
/// <returns>True if component was removed successfully</returns>
public static bool RemoveComponent(GameObject target, Type componentType, out string error)
{
error = null;
if (target == null)
{
error = "Target GameObject is null.";
return false;
}
if (componentType == null)
{
error = "Component type is null.";
return false;
}
// Prevent removing Transform
if (componentType == typeof(Transform))
{
error = "Cannot remove Transform component.";
return false;
}
Component component = target.GetComponent(componentType);
if (component == null)
{
error = $"Component '{componentType.Name}' not found on '{target.name}'.";
return false;
}
try
{
Undo.DestroyObjectImmediate(component);
return true;
}
catch (Exception ex)
{
error = $"Error removing component '{componentType.Name}': {ex.Message}";
return false;
}
}
/// <summary>
/// Sets a property value on a component using reflection.
/// </summary>
/// <param name="component">The target component</param>
/// <param name="propertyName">The property or field name</param>
/// <param name="value">The value to set (JToken)</param>
/// <param name="error">Error message if operation fails</param>
/// <returns>True if property was set successfully</returns>
public static bool SetProperty(Component component, string propertyName, JToken value, out string error)
{
error = null;
if (component == null)
{
error = "Component is null.";
return false;
}
if (string.IsNullOrEmpty(propertyName))
{
error = "Property name is null or empty.";
return false;
}
Type type = component.GetType();
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
string normalizedName = ParamCoercion.NormalizePropertyName(propertyName);
// UnityEventBase-derived types must be set via SerializedProperty, not reflection.
// Reflection creates a disconnected object that Unity's serialization layer doesn't track,
// causing m_PersistentCalls to be empty when the scene is saved.
Type memberType = ResolveMemberType(type, propertyName, normalizedName);
if (memberType != null && typeof(UnityEventBase).IsAssignableFrom(memberType))
{
return SetViaSerializedProperty(component, propertyName, normalizedName, value, out error);
}
// Try reflection first (property, field, then non-public serialized field)
if (TrySetViaReflection(component, type, propertyName, normalizedName, flags, value, out error))
return true;
// Reflection failed — fall back to SerializedProperty which handles arrays,
// custom serialization (e.g. UdonSharp), and types reflection can't convert.
string reflectionError = error;
if (SetViaSerializedProperty(component, propertyName, normalizedName, value, out error))
return true;
// Both paths failed. If reflection found the member but couldn't convert,
// report that (more useful than the SerializedProperty error).
// If reflection didn't find it at all, report the SerializedProperty error.
if (reflectionError != null && !reflectionError.Contains("not found"))
error = reflectionError;
return false;
}
private static bool TrySetViaReflection(object component, Type type, string propertyName, string normalizedName, BindingFlags flags, JToken value, out string error)
{
error = null;
// Skip reflection for UnityEngine.Object types with JObject values
// so SerializedProperty can resolve guid/spriteName/fileID forms.
bool isJObjectValue = value != null && value.Type == JTokenType.Object;
// Try property first
PropertyInfo propInfo = type.GetProperty(propertyName, flags)
?? type.GetProperty(normalizedName, flags);
if (propInfo != null && propInfo.CanWrite)
{
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(propInfo.PropertyType))
{
// Let SerializedProperty path handle complex object references.
return false;
}
try
{
object convertedValue = PropertyConversion.ConvertToType(value, propInfo.PropertyType);
if (convertedValue == null && value.Type != JTokenType.Null)
{
error = $"Failed to convert value for property '{propertyName}' to type '{propInfo.PropertyType.Name}'.";
return false;
}
propInfo.SetValue(component, convertedValue);
return true;
}
catch (Exception ex)
{
error = $"Failed to set property '{propertyName}': {ex.Message}";
return false;
}
}
// Try field
FieldInfo fieldInfo = type.GetField(propertyName, flags)
?? type.GetField(normalizedName, flags);
if (fieldInfo != null && !fieldInfo.IsInitOnly)
{
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(fieldInfo.FieldType))
{
// Let SerializedProperty path handle complex object references.
return false;
}
try
{
object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType);
if (convertedValue == null && value.Type != JTokenType.Null)
{
error = $"Failed to convert value for field '{propertyName}' to type '{fieldInfo.FieldType.Name}'.";
return false;
}
fieldInfo.SetValue(component, convertedValue);
return true;
}
catch (Exception ex)
{
error = $"Failed to set field '{propertyName}': {ex.Message}";
return false;
}
}
// Try non-public serialized fields — traverse inheritance hierarchy
fieldInfo = FindSerializedFieldInHierarchy(type, propertyName)
?? FindSerializedFieldInHierarchy(type, normalizedName);
if (fieldInfo != null)
{
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(fieldInfo.FieldType))
{
// Let SerializedProperty path handle complex object references.
return false;
}
try
{
object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType);
if (convertedValue == null && value.Type != JTokenType.Null)
{
error = $"Failed to convert value for serialized field '{propertyName}' to type '{fieldInfo.FieldType.Name}'.";
return false;
}
fieldInfo.SetValue(component, convertedValue);
return true;
}
catch (Exception ex)
{
error = $"Failed to set serialized field '{propertyName}': {ex.Message}";
return false;
}
}
error = $"Property or field '{propertyName}' not found on component '{type.Name}'.";
return false;
}
/// <summary>
/// Gets all public properties and fields from a component type.
/// </summary>
public static List<string> GetAccessibleMembers(Type componentType)
{
var members = new List<string>();
if (componentType == null) return members;
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
foreach (var prop in componentType.GetProperties(flags))
{
if (prop.CanWrite && prop.GetSetMethod() != null)
{
members.Add(prop.Name);
}
}
foreach (var field in componentType.GetFields(flags))
{
if (!field.IsInitOnly)
{
members.Add(field.Name);
}
}
// Include private [SerializeField] fields - traverse inheritance hierarchy
// Type.GetFields with NonPublic only returns fields declared directly on that type,
// so we need to walk up the chain to find inherited private serialized fields
var seenFieldNames = new HashSet<string>(members); // Avoid duplicates with public fields
Type currentType = componentType;
while (currentType != null && currentType != typeof(object))
{
foreach (var field in currentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
if (field.GetCustomAttribute<SerializeField>() != null && !seenFieldNames.Contains(field.Name))
{
members.Add(field.Name);
seenFieldNames.Add(field.Name);
}
}
currentType = currentType.BaseType;
}
members.Sort();
return members;
}
// --- Private Helpers ---
/// <summary>
/// Searches for a non-public [SerializeField] field through the entire inheritance hierarchy.
/// Type.GetField() with NonPublic only returns fields declared directly on that type,
/// so this method walks up the chain to find inherited private serialized fields.
/// </summary>
internal static FieldInfo FindSerializedFieldInHierarchy(Type type, string fieldName)
{
if (type == null || string.IsNullOrEmpty(fieldName))
return null;
BindingFlags privateFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
Type currentType = type;
// Walk up the inheritance chain
while (currentType != null && currentType != typeof(object))
{
// Search for the field on this specific type (case-insensitive)
foreach (var field in currentType.GetFields(privateFlags))
{
if (string.Equals(field.Name, fieldName, StringComparison.OrdinalIgnoreCase) &&
field.GetCustomAttribute<SerializeField>() != null)
{
return field;
}
}
currentType = currentType.BaseType;
}
return null;
}
private static string CheckPhysicsConflict(GameObject target, Type componentType)
{
bool isAdding2DPhysics =
typeof(Rigidbody2D).IsAssignableFrom(componentType) ||
typeof(Collider2D).IsAssignableFrom(componentType);
bool isAdding3DPhysics =
typeof(Rigidbody).IsAssignableFrom(componentType) ||
typeof(Collider).IsAssignableFrom(componentType);
if (isAdding2DPhysics)
{
if (target.GetComponent<Rigidbody>() != null || target.GetComponent<Collider>() != null)
{
return $"Cannot add 2D physics component '{componentType.Name}' because the GameObject '{target.name}' already has a 3D Rigidbody or Collider.";
}
}
else if (isAdding3DPhysics)
{
if (target.GetComponent<Rigidbody2D>() != null || target.GetComponent<Collider2D>() != null)
{
return $"Cannot add 3D physics component '{componentType.Name}' because the GameObject '{target.name}' already has a 2D Rigidbody or Collider.";
}
}
return null;
}
private static void ApplyDefaultValues(Component component)
{
// Default newly added Lights to Directional
if (component is Light light)
{
light.type = LightType.Directional;
}
}
private static bool AllowsMultiple(GameObject target, Type componentType)
{
if (target == null || componentType == null)
{
return false;
}
if (Attribute.IsDefined(componentType, typeof(DisallowMultipleComponent), inherit: true))
{
return false;
}
return true;
}
// --- UnityEvent SerializedProperty support ---
private static Type ResolveMemberType(Type componentType, string propertyName, string normalizedName)
{
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
PropertyInfo propInfo = componentType.GetProperty(propertyName, flags)
?? componentType.GetProperty(normalizedName, flags);
if (propInfo != null)
return propInfo.PropertyType;
FieldInfo fieldInfo = componentType.GetField(propertyName, flags)
?? componentType.GetField(normalizedName, flags);
if (fieldInfo != null)
return fieldInfo.FieldType;
fieldInfo = FindSerializedFieldInHierarchy(componentType, propertyName)
?? FindSerializedFieldInHierarchy(componentType, normalizedName);
if (fieldInfo != null)
return fieldInfo.FieldType;
return null;
}
private static bool SetViaSerializedProperty(Component component, string propertyName, string normalizedName, JToken value, out string error)
{
error = null;
using var so = new SerializedObject(component);
SerializedProperty prop = so.FindProperty(propertyName)
?? so.FindProperty(normalizedName);
if (prop == null)
{
error = $"SerializedProperty '{propertyName}' not found on component '{component.GetType().Name}'.";
return false;
}
if (!SetSerializedPropertyRecursive(prop, value, out error, 0))
return false;
so.ApplyModifiedProperties();
// Readback verification for ObjectReference — these can silently fail
if (prop.propertyType == SerializedPropertyType.ObjectReference
&& value != null
&& !(value is JValue jv && jv.Type == JTokenType.Null))
{
so.Update();
var verifyProp = so.FindProperty(propertyName)
?? so.FindProperty(normalizedName);
if (verifyProp != null
&& verifyProp.propertyType == SerializedPropertyType.ObjectReference
&& verifyProp.objectReferenceValue == null)
{
error = $"Property '{propertyName}' was set but the object reference did not persist. " +
"Check that the referenced object exists and is the correct type.";
return false;
}
}
return true;
}
private static bool SetSerializedPropertyRecursive(SerializedProperty prop, JToken value, out string error, int depth)
{
error = null;
const int MaxDepth = 20;
if (depth > MaxDepth)
{
error = $"Maximum recursion depth ({MaxDepth}) exceeded.";
return false;
}
try
{
// Array + JArray
if (prop.isArray && prop.propertyType != SerializedPropertyType.String && value is JArray jArray)
{
prop.arraySize = jArray.Count;
prop.serializedObject.ApplyModifiedProperties();
prop.serializedObject.Update();
for (int i = 0; i < jArray.Count; i++)
{
var element = prop.GetArrayElementAtIndex(i);
if (!SetSerializedPropertyRecursive(element, jArray[i], out error, depth + 1))
return false;
}
return true;
}
// Generic (struct/class) + JObject
if (prop.propertyType == SerializedPropertyType.Generic && !prop.isArray && value is JObject jObj)
{
foreach (var kvp in jObj)
{
var child = FindPropertyRelativeFuzzy(prop, kvp.Key);
if (child == null)
{
error = $"Sub-property '{kvp.Key}' not found under '{prop.propertyPath}'.";
return false;
}
if (!SetSerializedPropertyRecursive(child, kvp.Value, out error, depth + 1))
return false;
}
return true;
}
// ObjectReference
if (prop.propertyType == SerializedPropertyType.ObjectReference)
return SetObjectReference(prop, value, out error);
// Leaf types
switch (prop.propertyType)
{
case SerializedPropertyType.Integer:
if (value == null || value.Type == JTokenType.Null
|| (value.Type != JTokenType.Integer && value.Type != JTokenType.Float
&& !long.TryParse(value.ToString(), out _)))
{
error = "Expected integer value.";
return false;
}
if (prop.type == "long")
prop.longValue = ParamCoercion.CoerceLong(value, 0);
else
prop.intValue = ParamCoercion.CoerceInt(value, 0);
return true;
case SerializedPropertyType.Boolean:
if (value == null || value.Type == JTokenType.Null)
{
error = "Expected boolean value.";
return false;
}
prop.boolValue = ParamCoercion.CoerceBool(value, false);
return true;
case SerializedPropertyType.Float:
float floatVal = ParamCoercion.CoerceFloat(value, float.NaN);
if (float.IsNaN(floatVal))
{
error = "Expected float value.";
return false;
}
prop.floatValue = floatVal;
return true;
case SerializedPropertyType.String:
prop.stringValue = value == null || value.Type == JTokenType.Null ? string.Empty : value.ToString();
return true;
case SerializedPropertyType.Enum:
return SetEnum(prop, value, out error);
default:
error = $"Unsupported SerializedPropertyType: {prop.propertyType} at '{prop.propertyPath}'.";
return false;
}
}
catch (Exception ex)
{
error = $"Error setting '{prop.propertyPath}': {ex.Message}";
return false;
}
}
internal static bool SetObjectReference(SerializedProperty prop, JToken value, out string error)
{
error = null;
if (value == null || value.Type == JTokenType.Null)
{
prop.objectReferenceValue = null;
return true;
}
if (value.Type == JTokenType.Integer)
{
int id = value.Value<int>();
var resolved = GameObjectLookup.ResolveInstanceID(id);
if (resolved == null)
{
error = $"No object found with instanceID {id}.";
return false;
}
return AssignObjectReference(prop, resolved, null, out error);
}
if (value is JObject jObj)
{
// Optional component type filter — e.g. {"instanceID": 123, "component": "Button"}
string componentFilter = jObj["component"]?.ToString();
var idToken = jObj["instanceID"];
if (idToken != null)
{
int id = ParamCoercion.CoerceInt(idToken, 0);
var resolved = GameObjectLookup.ResolveInstanceID(id);
if (resolved == null)
{
error = $"No object found with instanceID {id}.";
return false;
}
return AssignObjectReference(prop, resolved, componentFilter, out error);
}
var guidToken = jObj["guid"];
if (guidToken != null)
{
string path = AssetDatabase.GUIDToAssetPath(guidToken.ToString());
if (string.IsNullOrEmpty(path))
{
error = $"No asset found for GUID '{guidToken}'.";
return false;
}
var spriteNameToken = jObj["spriteName"];
if (spriteNameToken != null)
{
string spriteName = spriteNameToken.ToString();
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
var originalRef = prop.objectReferenceValue;
foreach (var asset in allAssets)
{
if (asset is Sprite sprite && sprite.name == spriteName)
{
prop.objectReferenceValue = sprite;
if (prop.objectReferenceValue != null)
return true;
// Unity rejected the type — restore and report
prop.objectReferenceValue = originalRef;
error = $"Sprite '{spriteName}' found but is not compatible with the property type.";
return false;
}
}
error = $"Sprite '{spriteName}' not found in atlas '{path}'.";
return false;
}
var fileIdToken = jObj["fileID"];
if (fileIdToken != null)
{
long targetFileId = fileIdToken.Value<long>();
if (targetFileId != 0)
{
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
var originalRef = prop.objectReferenceValue;
foreach (var asset in allAssets)
{
if (asset is Sprite sprite)
{
long spriteFileId = GetSpriteFileId(sprite);
if (spriteFileId == targetFileId)
{
prop.objectReferenceValue = sprite;
if (prop.objectReferenceValue != null)
return true;
prop.objectReferenceValue = originalRef;
error = $"Sprite with fileID '{targetFileId}' found but is not compatible with the property type.";
return false;
}
}
}
}
error = $"Sprite with fileID '{targetFileId}' not found in atlas '{path}'.";
return false;
}
var loaded = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
return AssignObjectReference(prop, loaded, componentFilter, out error);
}
var pathToken = jObj["path"];
if (pathToken != null)
{
string sanitized = AssetPathUtility.SanitizeAssetPath(pathToken.ToString());
var resolved = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(sanitized);
if (resolved == null)
{
error = $"No asset found at path '{pathToken}'.";
return false;
}
return AssignObjectReference(prop, resolved, componentFilter, out error);
}
var nameToken = jObj["name"];
if (nameToken != null)
{
return ResolveSceneObjectByName(prop, nameToken.ToString(), componentFilter, out error);
}
error = "Object reference must contain 'instanceID', 'guid', 'path', or 'name'.";
return false;
}
if (value.Type == JTokenType.String)
{
string strVal = value.ToString();
// Try as instanceID if the string is purely numeric
if (int.TryParse(strVal, out int parsedId))
{
var resolved = GameObjectLookup.ResolveInstanceID(parsedId);
if (resolved != null)
return AssignObjectReference(prop, resolved, null, out error);
// Not a valid instanceID — fall through to path/name resolution
}
if (strVal.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) || strVal.Contains("/"))
{
string sanitized = AssetPathUtility.SanitizeAssetPath(strVal);
var resolved = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(sanitized);
if (resolved == null)
{
error = $"No asset found at path '{strVal}'.";
return false;
}
return AssignObjectReference(prop, resolved, null, out error);
}
// Try as asset GUID (32-char hex string)
if (strVal.Length == 32 && IsHexString(strVal))
{
string assetPath = AssetDatabase.GUIDToAssetPath(strVal);
if (!string.IsNullOrEmpty(assetPath))
{
var resolved = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
if (resolved != null)
return AssignObjectReference(prop, resolved, null, out error);
}
}
// Fall back to scene hierarchy lookup by name.
return ResolveSceneObjectByName(prop, strVal, null, out error);
}
error = $"Unsupported object reference format: {value.Type}.";
return false;
}
/// <summary>
/// Assigns a resolved object to a SerializedProperty, with automatic component fallback.
/// If the resolved object is a GameObject but the property expects a Component type,
/// searches the GameObject's components for a compatible one.
/// Optionally filters by component type name (e.g. "Button", "Rigidbody").
/// </summary>
private static bool AssignObjectReference(SerializedProperty prop, UnityEngine.Object resolved, string componentFilter, out string error)
{
error = null;
if (resolved == null)
{
error = "Resolved object is null.";
return false;
}
// If a component filter is specified and the resolved object is a GameObject,
// find the specific component by type name.
if (!string.IsNullOrEmpty(componentFilter) && resolved is GameObject filterGo)
{
var components = filterGo.GetComponents<Component>();
foreach (var comp in components)
{
if (comp == null) continue;
if (string.Equals(comp.GetType().Name, componentFilter, StringComparison.OrdinalIgnoreCase) ||
string.Equals(comp.GetType().FullName, componentFilter, StringComparison.OrdinalIgnoreCase))
{
prop.objectReferenceValue = comp;
if (prop.objectReferenceValue != null)
return true;
}
}
error = $"Component '{componentFilter}' not found on GameObject '{filterGo.name}'.";
return false;
}
// Try direct assignment first
prop.objectReferenceValue = resolved;
if (prop.objectReferenceValue != null)
return true;
// Sub-asset fallback: e.g., Texture2D → Sprite
string subAssetPath = AssetDatabase.GetAssetPath(resolved);
if (!string.IsNullOrEmpty(subAssetPath))
{
var subAssets = AssetDatabase.LoadAllAssetsAtPath(subAssetPath);
UnityEngine.Object match = null;
int matchCount = 0;
foreach (var sub in subAssets)
{
if (sub == null || sub == resolved) continue;
prop.objectReferenceValue = sub;
if (prop.objectReferenceValue != null)
{
match = sub;
matchCount++;
if (matchCount > 1) break;
}
}
if (matchCount == 1)
{
prop.objectReferenceValue = match;
return true;
}
// Clean up: probing may have left the property dirty
prop.objectReferenceValue = null;
if (matchCount > 1)
{
error = $"Multiple compatible sub-assets found in '{subAssetPath}'. " +
"Use {\"guid\": \"...\", \"spriteName\": \"<name>\"} or " +
"{\"guid\": \"...\", \"fileID\": <id>} for precise selection.";
return false;
}
}
// If the resolved object is a GameObject but the property expects a Component,
// try each component on the GameObject until one is accepted.
if (resolved is GameObject go)
{
var components = go.GetComponents<Component>();
foreach (var comp in components)
{
if (comp == null) continue;
prop.objectReferenceValue = comp;
if (prop.objectReferenceValue != null)
return true;
}
error = $"GameObject '{go.name}' found but no compatible component for the property type.";
return false;
}
error = $"Object '{resolved.name}' (type: {resolved.GetType().Name}) is not compatible with the property type.";
return false;
}
/// <summary>
/// Resolves a scene GameObject by name and assigns it (or a component on it)
/// to a SerializedProperty. Uses GameObjectLookup for robust search
/// including inactive objects and prefab stage support.
/// </summary>
private static bool ResolveSceneObjectByName(SerializedProperty prop, string name, string componentFilter, out string error)
{
error = null;
if (string.IsNullOrWhiteSpace(name))
{
error = "Cannot resolve object reference from empty name.";
return false;
}
var ids = GameObjectLookup.SearchGameObjects(
GameObjectLookup.SearchMethod.ByName, name, includeInactive: true, maxResults: 1);
if (ids.Count == 0)
{
error = $"No GameObject named '{name}' found in scene.";
return false;
}
var go = GameObjectLookup.FindById(ids[0]);
if (go == null)
{
error = $"GameObject '{name}' found but could not be resolved.";
return false;
}
return AssignObjectReference(prop, go, componentFilter, out error);
}
/// <summary>
/// Finds a child SerializedProperty by name, falling back to underscore-insensitive matching.
/// The batch_execute transport can strip underscores from JSON keys
/// (e.g. m_PersistentCalls → mPersistentCalls), so we iterate immediate children
/// and compare with underscores removed.
/// </summary>
private static SerializedProperty FindPropertyRelativeFuzzy(SerializedProperty parent, string key)
{
var child = parent.FindPropertyRelative(key);
if (child != null) return child;
string normalizedKey = key.Replace("_", "").ToLowerInvariant();
var end = parent.GetEndProperty();
var iter = parent.Copy();
if (!iter.Next(true)) return null;
while (!SerializedProperty.EqualContents(iter, end))
{
if (iter.depth == parent.depth + 1)
{
string normalizedName = iter.name.Replace("_", "").ToLowerInvariant();
if (normalizedName == normalizedKey)
return parent.FindPropertyRelative(iter.name);
}
if (!iter.Next(false))
break;
}
return null;
}
private static bool SetEnum(SerializedProperty prop, JToken value, out string error)
{
error = null;
var names = prop.enumNames;
if (names == null || names.Length == 0)
{
error = "Enum has no names.";
return false;
}
if (value.Type == JTokenType.Integer)
{
int idx = value.Value<int>();
if (idx < 0 || idx >= names.Length)
{
error = $"Enum index out of range: {idx}.";
return false;
}
prop.enumValueIndex = idx;
return true;
}
string s = value.ToString();
for (int i = 0; i < names.Length; i++)
{
if (string.Equals(names[i], s, StringComparison.OrdinalIgnoreCase))
{
prop.enumValueIndex = i;
return true;
}
}
error = $"Unknown enum name '{s}'.";
return false;
}
private static long GetSpriteFileId(Sprite sprite)
{
if (sprite == null)
return 0;
try
{
var globalId = GlobalObjectId.GetGlobalObjectIdSlow(sprite);
return unchecked((long)globalId.targetObjectId);
}
catch (Exception ex)
{
McpLog.Warn($"Failed to get fileID for sprite '{sprite.name}' (instanceID={sprite.GetInstanceIDCompat()}): {ex.Message}");
return 0;
}
}
private static bool IsHexString(string str)
{
foreach (char c in str)
{
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
return false;
}
return true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 13dead161bc4540eeb771961df437779
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More