Rename plugin folder to MCPForUnity (#303)

* Copy UnityMcpBridge into a new MCPForUnity folder

This is to close #284

* refactor: rename UnityMcpBridge directory to MCPForUnity in docs

* chore: rename UnityMcpBridge directory to MCPForUnity across workflow files

* chore: rename UnityMcpBridge directory to MCPForUnity across all files

* refactor: update import paths from UnityMcpBridge to MCPForUnity across test files

* fix: update module import paths to use MCPForUnity instead of UnityMcpBridge

* chore: update unity-mcp package path to MCPForUnity directory

* feat: add OneTimeSetUp to initialize CommandRegistry before tests run

Hopefully fix the CI failures

* Apply recent fix to new folder

* Temporarily trigger tests to see if CI works

* Revert "Temporarily trigger tests to see if CI works"

It works!

This reverts commit 8c6eaaad07545cef047769f2c52fe506545a8161.
main
Marcus Sanatan 2025-10-03 20:23:28 -04:00 committed by GitHub
parent ff736012fa
commit e9b1ae44c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
197 changed files with 26599 additions and 55 deletions

View File

@ -31,7 +31,7 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
BUMP="${{ inputs.version_bump }}" BUMP="${{ inputs.version_bump }}"
CURRENT_VERSION=$(jq -r '.version' "UnityMcpBridge/package.json") CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
echo "Current version: $CURRENT_VERSION" echo "Current version: $CURRENT_VERSION"
IFS='.' read -r MA MI PA <<< "$CURRENT_VERSION" IFS='.' read -r MA MI PA <<< "$CURRENT_VERSION"
@ -63,15 +63,15 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
echo "Updating UnityMcpBridge/package.json to $NEW_VERSION" echo "Updating MCPForUnity/package.json to $NEW_VERSION"
jq ".version = \"${NEW_VERSION}\"" UnityMcpBridge/package.json > UnityMcpBridge/package.json.tmp jq ".version = \"${NEW_VERSION}\"" MCPForUnity/package.json > MCPForUnity/package.json.tmp
mv UnityMcpBridge/package.json.tmp UnityMcpBridge/package.json mv MCPForUnity/package.json.tmp MCPForUnity/package.json
echo "Updating UnityMcpBridge/UnityMcpServer~/src/pyproject.toml to $NEW_VERSION" echo "Updating MCPForUnity/UnityMcpServer~/src/pyproject.toml to $NEW_VERSION"
sed -i '0,/^version = ".*"/s//version = "'"$NEW_VERSION"'"/' "UnityMcpBridge/UnityMcpServer~/src/pyproject.toml" sed -i '0,/^version = ".*"/s//version = "'"$NEW_VERSION"'"/' "MCPForUnity/UnityMcpServer~/src/pyproject.toml"
echo "Updating UnityMcpBridge/UnityMcpServer~/src/server_version.txt to $NEW_VERSION" echo "Updating MCPForUnity/UnityMcpServer~/src/server_version.txt to $NEW_VERSION"
echo "$NEW_VERSION" > "UnityMcpBridge/UnityMcpServer~/src/server_version.txt" echo "$NEW_VERSION" > "MCPForUnity/UnityMcpServer~/src/server_version.txt"
- name: Commit and push changes - name: Commit and push changes
env: env:
@ -81,7 +81,7 @@ jobs:
set -euo pipefail set -euo pipefail
git config user.name "GitHub Actions" git config user.name "GitHub Actions"
git config user.email "actions@github.com" git config user.email "actions@github.com"
git add UnityMcpBridge/package.json "UnityMcpBridge/UnityMcpServer~/src/pyproject.toml" "UnityMcpBridge/UnityMcpServer~/src/server_version.txt" git add MCPForUnity/package.json "MCPForUnity/UnityMcpServer~/src/pyproject.toml" "MCPForUnity/UnityMcpServer~/src/server_version.txt"
if git diff --cached --quiet; then if git diff --cached --quiet; then
echo "No version changes to commit." echo "No version changes to commit."
else else

View File

@ -55,14 +55,14 @@ jobs:
uv venv uv venv
echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV" echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV"
echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH" echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"
if [ -f UnityMcpBridge/UnityMcpServer~/src/pyproject.toml ]; then if [ -f MCPForUnity/UnityMcpServer~/src/pyproject.toml ]; then
uv pip install -e UnityMcpBridge/UnityMcpServer~/src uv pip install -e MCPForUnity/UnityMcpServer~/src
elif [ -f UnityMcpBridge/UnityMcpServer~/src/requirements.txt ]; then elif [ -f MCPForUnity/UnityMcpServer~/src/requirements.txt ]; then
uv pip install -r UnityMcpBridge/UnityMcpServer~/src/requirements.txt uv pip install -r MCPForUnity/UnityMcpServer~/src/requirements.txt
elif [ -f UnityMcpBridge/UnityMcpServer~/pyproject.toml ]; then elif [ -f MCPForUnity/UnityMcpServer~/pyproject.toml ]; then
uv pip install -e UnityMcpBridge/UnityMcpServer~/ uv pip install -e MCPForUnity/UnityMcpServer~/
elif [ -f UnityMcpBridge/UnityMcpServer~/requirements.txt ]; then elif [ -f MCPForUnity/UnityMcpServer~/requirements.txt ]; then
uv pip install -r UnityMcpBridge/UnityMcpServer~/requirements.txt uv pip install -r MCPForUnity/UnityMcpServer~/requirements.txt
else else
echo "No MCP Python deps found (skipping)" echo "No MCP Python deps found (skipping)"
fi fi
@ -285,7 +285,7 @@ jobs:
"mcpServers": { "mcpServers": {
"unity": { "unity": {
"command": "uv", "command": "uv",
"args": ["run","--active","--directory","UnityMcpBridge/UnityMcpServer~/src","python","server.py"], "args": ["run","--active","--directory","MCPForUnity/UnityMcpServer~/src","python","server.py"],
"transport": { "type": "stdio" }, "transport": { "type": "stdio" },
"env": { "env": {
"PYTHONUNBUFFERED": "1", "PYTHONUNBUFFERED": "1",

View File

@ -5,7 +5,7 @@ on:
branches: [main] branches: [main]
paths: paths:
- TestProjects/UnityMCPTests/** - TestProjects/UnityMCPTests/**
- UnityMcpBridge/Editor/** - MCPForUnity/Editor/**
- .github/workflows/unity-tests.yml - .github/workflows/unity-tests.yml
jobs: jobs:

1
.gitignore vendored
View File

@ -25,7 +25,6 @@ UnityMcpServer.meta
# Unity Editor # Unity Editor
*.unitypackage *.unitypackage
*.asset *.asset
UnityMcpBridge.meta
LICENSE.meta LICENSE.meta
CONTRIBUTING.md.meta CONTRIBUTING.md.meta

8
MCPForUnity/Editor.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 31e7fac5858840340a75cc6df0ad3d9e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")]

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be61633e00d934610ac1ff8192ffbe3d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e59036660cc33d24596fbbf6d4657a83
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,17 @@
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Data
{
public class DefaultServerConfig : ServerConfig
{
public new string unityHost = "localhost";
public new int unityPort = 6400;
public new int mcpPort = 6500;
public new float connectionTimeout = 15.0f;
public new int bufferSize = 32768;
public new string logLevel = "INFO";
public new string logFormat = "%(asctime)s - %(name)s - %(levelname)s - %(message)s";
public new int maxRetries = 3;
public new float retryDelay = 1.0f;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: de8f5721c34f7194392e9d8c7d0226c0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Data
{
public class McpClients
{
public List<McpClient> clients = new()
{
// 1) Cursor
new()
{
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"
),
mcpType = McpTypes.Cursor,
configStatus = "Not Configured",
},
// 2) Claude Code
new()
{
name = "Claude Code",
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".claude.json"
),
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".claude.json"
),
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".claude.json"
),
mcpType = McpTypes.ClaudeCode,
configStatus = "Not Configured",
},
// 3) Windsurf
new()
{
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"
),
mcpType = McpTypes.Windsurf,
configStatus = "Not Configured",
},
// 4) Claude Desktop
new()
{
name = "Claude Desktop",
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"
),
mcpType = McpTypes.ClaudeDesktop,
configStatus = "Not Configured",
},
// 5) VSCode GitHub Copilot
new()
{
name = "VSCode GitHub Copilot",
// Windows path is canonical under %AppData%\Code\User
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Code",
"User",
"mcp.json"
),
// macOS: ~/Library/Application Support/Code/User/mcp.json
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library",
"Application Support",
"Code",
"User",
"mcp.json"
),
// Linux: ~/.config/Code/User/mcp.json
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config",
"Code",
"User",
"mcp.json"
),
mcpType = McpTypes.VSCode,
configStatus = "Not Configured",
},
// 3) Kiro
new()
{
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"
),
mcpType = McpTypes.Kiro,
configStatus = "Not Configured",
},
// 4) Codex CLI
new()
{
name = "Codex CLI",
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"
),
mcpType = McpTypes.Codex,
configStatus = "Not Configured",
},
};
// Initialize status enums after construction
public McpClients()
{
foreach (var client in clients)
{
if (client.configStatus == "Not Configured")
{
client.status = McpStatus.NotConfigured;
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 711b86bbc1f661e4fb2c822e14970e16
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 221a4d6e595be6897a5b17b77aedd4d0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,147 @@
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);
// Check MCP Server
var serverStatus = detector.DetectMCPServer();
result.Dependencies.Add(serverStatus);
// 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 Wizard (Window > MCP for Unity > Setup Wizard) for guided installation.");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f6789012345678901234abcdef012345
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b2c3d4e5f6789012345678901234abcd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 789012345678901234abcdef01234567
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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}";
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6789012345678901234abcdef0123456
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c3d4e5f6789012345678901234abcdef
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,50 @@
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>
/// Detect MCP server installation on this platform
/// </summary>
DependencyStatus DetectMCPServer();
/// <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();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9012345678901234abcdef0123456789
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,212 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
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
{
// Check common Python installation paths on Linux
var candidates = new[]
{
"python3",
"python",
"/usr/bin/python3",
"/usr/local/bin/python3",
"/opt/python/bin/python3",
"/snap/bin/python3"
};
foreach (var candidate in candidates)
{
if (TryValidatePython(candidate, out string version, out string fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} at {fullPath}";
return status;
}
}
// Try PATH resolution using 'which' command
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} in PATH at {fullPath}";
return status;
}
}
status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
status.Details = "Checked common installation paths including system, snap, and user-local locations.";
}
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 Unity MCP Bridge
Note: Make sure ~/.local/bin is in your PATH for user-local installations.";
}
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
var psi = new ProcessStartInfo
{
FileName = pythonPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
// Set PATH to include common locations
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var pathAdditions = new[]
{
"/usr/local/bin",
"/usr/bin",
"/bin",
"/snap/bin",
Path.Combine(homeDir, ".local", "bin")
};
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(5000);
if (process.ExitCode == 0 && output.StartsWith("Python "))
{
version = output.Substring(7); // Remove "Python " prefix
fullPath = pythonPath;
// Validate minimum version (Python 4+ or Python 3.10+)
if (TryParseVersion(version, out var major, out var minor))
{
return major > 3 || (major >= 3 && minor >= 10);
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
private bool TryFindInPath(string executable, out string fullPath)
{
fullPath = null;
try
{
var psi = new ProcessStartInfo
{
FileName = "/usr/bin/which",
Arguments = executable,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
// Enhance PATH for Unity's GUI environment
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var pathAdditions = new[]
{
"/usr/local/bin",
"/usr/bin",
"/bin",
"/snap/bin",
Path.Combine(homeDir, ".local", "bin")
};
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(3000);
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
{
fullPath = output;
return true;
}
}
catch
{
// Ignore errors
}
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2345678901234abcdef0123456789abc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,212 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
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
{
// Check common Python installation paths on macOS
var candidates = new[]
{
"python3",
"python",
"/usr/bin/python3",
"/usr/local/bin/python3",
"/opt/homebrew/bin/python3",
"/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
"/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
"/Library/Frameworks/Python.framework/Versions/3.11/bin/python3",
"/Library/Frameworks/Python.framework/Versions/3.10/bin/python3"
};
foreach (var candidate in candidates)
{
if (TryValidatePython(candidate, out string version, out string fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} at {fullPath}";
return status;
}
}
// Try PATH resolution using 'which' command
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} in PATH at {fullPath}";
return status;
}
}
status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
status.Details = "Checked common installation paths including Homebrew, Framework, and system locations.";
}
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 Unity MCP Bridge
Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH.";
}
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
var psi = new ProcessStartInfo
{
FileName = pythonPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
// Set PATH to include common locations
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var pathAdditions = new[]
{
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
Path.Combine(homeDir, ".local", "bin")
};
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(5000);
if (process.ExitCode == 0 && output.StartsWith("Python "))
{
version = output.Substring(7); // Remove "Python " prefix
fullPath = pythonPath;
// Validate minimum version (Python 4+ or Python 3.10+)
if (TryParseVersion(version, out var major, out var minor))
{
return major > 3 || (major >= 3 && minor >= 10);
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
private bool TryFindInPath(string executable, out string fullPath)
{
fullPath = null;
try
{
var psi = new ProcessStartInfo
{
FileName = "/usr/bin/which",
Arguments = executable,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
// Enhance PATH for Unity's GUI environment
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var pathAdditions = new[]
{
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
"/bin",
Path.Combine(homeDir, ".local", "bin")
};
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(3000);
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
{
fullPath = output;
return true;
}
}
catch
{
// Ignore errors
}
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 12345678901234abcdef0123456789ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,161 @@
using System;
using System.Diagnostics;
using System.IO;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
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
{
// Use existing UV detection from ServerInstaller
string uvPath = ServerInstaller.FindUvPath();
if (!string.IsNullOrEmpty(uvPath))
{
if (TryValidateUV(uvPath, out string version))
{
status.IsAvailable = true;
status.Version = version;
status.Path = uvPath;
status.Details = $"Found UV {version} at {uvPath}";
return status;
}
}
status.ErrorMessage = "UV package manager not found. Please install UV.";
status.Details = "UV is required for managing Python dependencies.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting UV: {ex.Message}";
}
return status;
}
public virtual DependencyStatus DetectMCPServer()
{
var status = new DependencyStatus("MCP Server", isRequired: false);
try
{
// Check if server is installed
string serverPath = ServerInstaller.GetServerPath();
string serverPy = Path.Combine(serverPath, "server.py");
if (File.Exists(serverPy))
{
status.IsAvailable = true;
status.Path = serverPath;
// Try to get version
string versionFile = Path.Combine(serverPath, "server_version.txt");
if (File.Exists(versionFile))
{
status.Version = File.ReadAllText(versionFile).Trim();
}
status.Details = $"MCP Server found at {serverPath}";
}
else
{
// Check for embedded server
if (ServerPathResolver.TryFindEmbeddedServerSource(out string embeddedPath))
{
status.IsAvailable = true;
status.Path = embeddedPath;
status.Details = "MCP Server available (embedded in package)";
}
else
{
status.ErrorMessage = "MCP Server not found";
status.Details = "Server will be installed automatically when needed";
}
}
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting MCP Server: {ex.Message}";
}
return status;
}
protected bool TryValidateUV(string uvPath, out string version)
{
version = null;
try
{
var psi = new ProcessStartInfo
{
FileName = uvPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(5000);
if (process.ExitCode == 0 && output.StartsWith("uv "))
{
version = output.Substring(3); // Remove "uv " prefix
return true;
}
}
catch
{
// Ignore validation errors
}
return false;
}
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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 44d715aedea2b8b41bf914433bbb2c49
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,191 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
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
{
// Check common Python installation paths
var candidates = new[]
{
"python.exe",
"python3.exe",
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Programs", "Python", "Python313", "python.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Programs", "Python", "Python312", "python.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Programs", "Python", "Python311", "python.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
"Python313", "python.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
"Python312", "python.exe")
};
foreach (var candidate in candidates)
{
if (TryValidatePython(candidate, out string version, out string fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} at {fullPath}";
return status;
}
}
// Try PATH resolution using 'where' command
if (TryFindInPath("python.exe", out string pathResult) ||
TryFindInPath("python3.exe", 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} in PATH at {fullPath}";
return status;
}
}
status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
status.Details = "Checked common installation paths and PATH environment variable.";
}
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.12' or 'Python 3.13'
- 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 Unity MCP Bridge";
}
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
var psi = new ProcessStartInfo
{
FileName = pythonPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(5000);
if (process.ExitCode == 0 && output.StartsWith("Python "))
{
version = output.Substring(7); // Remove "Python " prefix
fullPath = pythonPath;
// Validate minimum version (Python 4+ or Python 3.10+)
if (TryParseVersion(version, out var major, out var minor))
{
return major > 3 || (major >= 3 && minor >= 10);
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
private bool TryFindInPath(string executable, out string fullPath)
{
fullPath = null;
try
{
var psi = new ProcessStartInfo
{
FileName = "where",
Arguments = executable,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(3000);
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output))
{
// Take the first result
var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
if (lines.Length > 0)
{
fullPath = lines[0].Trim();
return File.Exists(fullPath);
}
}
}
catch
{
// Ignore errors
}
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 012345678901234abcdef0123456789a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c11944bcfb9ec4576bab52874b7df584
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

2138
MCPForUnity/Editor/External/Tommy.cs vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ea652131dcdaa44ca8cb35cd1191be3f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 94cb070dc5e15024da86150b27699ca0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,29 @@
using System;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Provides common utility methods for working with Unity asset paths.
/// </summary>
public static class AssetPathUtility
{
/// <summary>
/// Normalizes a Unity asset path by ensuring forward slashes are used and that it is rooted under "Assets/".
/// </summary>
public static string SanitizeAssetPath(string path)
{
if (string.IsNullOrEmpty(path))
{
return path;
}
path = path.Replace('\\', '/');
if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
return "Assets/" + path.TrimStart('/');
}
return path;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d42f5b5ea5d4d43ad1a771e14bda2a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,243 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using MCPForUnity.External.Tommy;
using Newtonsoft.Json;
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
{
public static bool IsCodexConfigured(string pythonDir)
{
try
{
string basePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (string.IsNullOrEmpty(basePath)) return false;
string configPath = Path.Combine(basePath, ".codex", "config.toml");
if (!File.Exists(configPath)) return false;
string toml = File.ReadAllText(configPath);
if (!TryParseCodexServer(toml, out _, out var args)) return false;
string dir = McpConfigFileHelper.ExtractDirectoryArg(args);
if (string.IsNullOrEmpty(dir)) return false;
return McpConfigFileHelper.PathsEqual(dir, pythonDir);
}
catch
{
return false;
}
}
public static string BuildCodexServerBlock(string uvPath, string serverSrc)
{
string argsArray = FormatTomlStringArray(new[] { "run", "--directory", serverSrc, "server.py" });
return $"[mcp_servers.unityMCP]{Environment.NewLine}" +
$"command = \"{EscapeTomlString(uvPath)}\"{Environment.NewLine}" +
$"args = {argsArray}";
}
public static string UpsertCodexServerBlock(string existingToml, string newBlock)
{
if (string.IsNullOrWhiteSpace(existingToml))
{
return newBlock.TrimEnd() + Environment.NewLine;
}
StringBuilder sb = new StringBuilder();
using StringReader reader = new StringReader(existingToml);
string line;
bool inTarget = false;
bool replaced = false;
while ((line = reader.ReadLine()) != null)
{
string trimmed = line.Trim();
bool isSection = trimmed.StartsWith("[") && trimmed.EndsWith("]") && !trimmed.StartsWith("[[");
if (isSection)
{
bool isTarget = string.Equals(trimmed, "[mcp_servers.unityMCP]", StringComparison.OrdinalIgnoreCase);
if (isTarget)
{
if (!replaced)
{
if (sb.Length > 0 && sb[^1] != '\n') sb.AppendLine();
sb.AppendLine(newBlock.TrimEnd());
replaced = true;
}
inTarget = true;
continue;
}
if (inTarget)
{
inTarget = false;
}
}
if (inTarget)
{
continue;
}
sb.AppendLine(line);
}
if (!replaced)
{
if (sb.Length > 0 && sb[^1] != '\n') sb.AppendLine();
sb.AppendLine(newBlock.TrimEnd());
}
return sb.ToString().TrimEnd() + Environment.NewLine;
}
public static bool TryParseCodexServer(string toml, out string command, out string[] args)
{
command = null;
args = null;
if (string.IsNullOrWhiteSpace(toml)) return false;
try
{
using var reader = new StringReader(toml);
TomlTable root = TOML.Parse(reader);
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;
}
command = GetTomlString(unity, "command");
args = GetTomlStringArray(unity, "args");
return !string.IsNullOrEmpty(command) && args != null;
}
catch (TomlParseException)
{
return false;
}
catch (TomlSyntaxException)
{
return false;
}
catch (FormatException)
{
return false;
}
}
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;
}
private static string FormatTomlStringArray(IEnumerable<string> values)
{
if (values == null) return "[]";
StringBuilder sb = new StringBuilder();
sb.Append('[');
bool first = true;
foreach (string value in values)
{
if (!first)
{
sb.Append(", ");
}
sb.Append('"').Append(EscapeTomlString(value ?? string.Empty)).Append('"');
first = false;
}
sb.Append(']');
return sb.ToString();
}
private static string EscapeTomlString(string value)
{
if (string.IsNullOrEmpty(value)) return string.Empty;
return value
.Replace("\\", "\\\\")
.Replace("\"", "\\\"");
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3e68082ffc0b4cd39d3747673a4cc22
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,129 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Helpers
{
public static class ConfigJsonBuilder
{
public static string BuildManualConfigJson(string uvPath, string pythonDir, McpClient client)
{
var root = new JObject();
bool isVSCode = client?.mcpType == McpTypes.VSCode;
JObject container;
if (isVSCode)
{
container = EnsureObject(root, "servers");
}
else
{
container = EnsureObject(root, "mcpServers");
}
var unity = new JObject();
PopulateUnityNode(unity, uvPath, pythonDir, client, isVSCode);
container["unityMCP"] = unity;
return root.ToString(Formatting.Indented);
}
public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, string serverSrc, McpClient client)
{
if (root == null) root = new JObject();
bool isVSCode = client?.mcpType == McpTypes.VSCode;
JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
JObject unity = container["unityMCP"] as JObject ?? new JObject();
PopulateUnityNode(unity, uvPath, serverSrc, client, isVSCode);
container["unityMCP"] = unity;
return root;
}
/// <summary>
/// Centralized builder that applies all caveats consistently.
/// - Sets command/args with provided directory
/// - Ensures env exists
/// - Adds type:"stdio" for VSCode
/// - Adds disabled:false for Windsurf/Kiro only when missing
/// </summary>
private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode)
{
unity["command"] = uvPath;
// For Cursor (non-VSCode) on macOS, prefer a no-spaces symlink path to avoid arg parsing issues in some runners
string effectiveDir = directory;
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
bool isCursor = !isVSCode && (client == null || client.mcpType != McpTypes.VSCode);
if (isCursor && !string.IsNullOrEmpty(directory))
{
// Replace canonical path segment with the symlink path if present
const string canonical = "/Library/Application Support/";
const string symlinkSeg = "/Library/AppSupport/";
try
{
// Normalize to full path style
if (directory.Contains(canonical))
{
var candidate = directory.Replace(canonical, symlinkSeg).Replace('\\', '/');
if (System.IO.Directory.Exists(candidate))
{
effectiveDir = candidate;
}
}
else
{
// If installer returned XDG-style on macOS, map to canonical symlink
string norm = directory.Replace('\\', '/');
int idx = norm.IndexOf("/.local/share/UnityMCP/", System.StringComparison.Ordinal);
if (idx >= 0)
{
string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal) ?? string.Empty;
string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/...
string candidate = System.IO.Path.Combine(home, "Library", "AppSupport", suffix).Replace('\\', '/');
if (System.IO.Directory.Exists(candidate))
{
effectiveDir = candidate;
}
}
}
}
catch { /* fallback to original directory on any error */ }
}
#endif
unity["args"] = JArray.FromObject(new[] { "run", "--directory", effectiveDir, "server.py" });
if (isVSCode)
{
unity["type"] = "stdio";
}
else
{
// Remove type if it somehow exists from previous clients
if (unity["type"] != null) unity.Remove("type");
}
if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro))
{
if (unity["env"] == null)
{
unity["env"] = new JObject();
}
if (unity["disabled"] == null)
{
unity["disabled"] = false;
}
}
}
private static JObject EnsureObject(JObject parent, string name)
{
if (parent[name] is JObject o) return o;
var created = new JObject();
parent[name] = created;
return created;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5c07c3369f73943919d9e086a81d1dcc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,278 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using UnityEditor;
namespace MCPForUnity.Editor.Helpers
{
internal static class ExecPath
{
private const string PrefClaude = "MCPForUnity.ClaudeCliPath";
// Resolve Claude CLI absolute path. Pref → env → common locations → PATH.
internal static string ResolveClaude()
{
try
{
string pref = EditorPrefs.GetString(PrefClaude, string.Empty);
if (!string.IsNullOrEmpty(pref) && File.Exists(pref)) return pref;
}
catch { }
string env = Environment.GetEnvironmentVariable("CLAUDE_CLI");
if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
string[] candidates =
{
"/opt/homebrew/bin/claude",
"/usr/local/bin/claude",
Path.Combine(home, ".local", "bin", "claude"),
};
foreach (string c in candidates) { if (File.Exists(c)) return c; }
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
string nvmClaude = ResolveClaudeFromNvm(home);
if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin");
#else
return null;
#endif
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
#if UNITY_EDITOR_WIN
// Common npm global locations
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
string[] candidates =
{
// Prefer .cmd (most reliable from non-interactive processes)
Path.Combine(appData, "npm", "claude.cmd"),
Path.Combine(localAppData, "npm", "claude.cmd"),
// Fall back to PowerShell shim if only .ps1 is present
Path.Combine(appData, "npm", "claude.ps1"),
Path.Combine(localAppData, "npm", "claude.ps1"),
};
foreach (string c in candidates) { if (File.Exists(c)) return c; }
string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude.ps1") ?? Where("claude");
if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
#endif
return null;
}
// Linux
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
string[] candidates =
{
"/usr/local/bin/claude",
"/usr/bin/claude",
Path.Combine(home, ".local", "bin", "claude"),
};
foreach (string c in candidates) { if (File.Exists(c)) return c; }
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
string nvmClaude = ResolveClaudeFromNvm(home);
if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
return Which("claude", "/usr/local/bin:/usr/bin:/bin");
#else
return null;
#endif
}
}
// Attempt to resolve claude from NVM-managed Node installations, choosing the newest version
private static string ResolveClaudeFromNvm(string home)
{
try
{
if (string.IsNullOrEmpty(home)) return null;
string nvmNodeDir = Path.Combine(home, ".nvm", "versions", "node");
if (!Directory.Exists(nvmNodeDir)) return null;
string bestPath = null;
Version bestVersion = null;
foreach (string versionDir in Directory.EnumerateDirectories(nvmNodeDir))
{
string name = Path.GetFileName(versionDir);
if (string.IsNullOrEmpty(name)) continue;
if (name.StartsWith("v", StringComparison.OrdinalIgnoreCase))
{
// Extract numeric portion: e.g., v18.19.0-nightly -> 18.19.0
string versionStr = name.Substring(1);
int dashIndex = versionStr.IndexOf('-');
if (dashIndex > 0)
{
versionStr = versionStr.Substring(0, dashIndex);
}
if (Version.TryParse(versionStr, out Version parsed))
{
string candidate = Path.Combine(versionDir, "bin", "claude");
if (File.Exists(candidate))
{
if (bestVersion == null || parsed > bestVersion)
{
bestVersion = parsed;
bestPath = candidate;
}
}
}
}
}
return bestPath;
}
catch { return null; }
}
// Explicitly set the Claude CLI absolute path override in EditorPrefs
internal static void SetClaudeCliPath(string absolutePath)
{
try
{
if (!string.IsNullOrEmpty(absolutePath) && File.Exists(absolutePath))
{
EditorPrefs.SetString(PrefClaude, absolutePath);
}
}
catch { }
}
// Clear any previously set Claude CLI override path
internal static void ClearClaudeCliPath()
{
try
{
if (EditorPrefs.HasKey(PrefClaude))
{
EditorPrefs.DeleteKey(PrefClaude);
}
}
catch { }
}
// Use existing UV resolver; returns absolute path or null.
internal static string ResolveUv()
{
return ServerInstaller.FindUvPath();
}
internal static bool TryRun(
string file,
string args,
string workingDir,
out string stdout,
out string stderr,
int timeoutMs = 15000,
string extraPathPrepend = null)
{
stdout = string.Empty;
stderr = string.Empty;
try
{
// Handle PowerShell scripts on Windows by invoking through powershell.exe
bool isPs1 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
file.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase);
var psi = new ProcessStartInfo
{
FileName = isPs1 ? "powershell.exe" : file,
Arguments = isPs1
? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim()
: args,
WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
if (!string.IsNullOrEmpty(extraPathPrepend))
{
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath)
? extraPathPrepend
: (extraPathPrepend + System.IO.Path.PathSeparator + currentPath);
}
using var process = new Process { StartInfo = psi, EnableRaisingEvents = false };
var so = new StringBuilder();
var se = new StringBuilder();
process.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); };
if (!process.Start()) return false;
process.BeginOutputReadLine();
process.BeginErrorReadLine();
if (!process.WaitForExit(timeoutMs))
{
try { process.Kill(); } catch { }
return false;
}
// Ensure async buffers are flushed
process.WaitForExit();
stdout = so.ToString();
stderr = se.ToString();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
private static string Which(string exe, string prependPath)
{
try
{
var psi = new ProcessStartInfo("/usr/bin/which", exe)
{
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
};
string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path);
using var p = Process.Start(psi);
string output = p?.StandardOutput.ReadToEnd().Trim();
p?.WaitForExit(1500);
return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null;
}
catch { return null; }
}
#endif
#if UNITY_EDITOR_WIN
private static string Where(string exe)
{
try
{
var psi = new ProcessStartInfo("where", exe)
{
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
};
using var p = Process.Start(psi);
string first = p?.StandardOutput.ReadToEnd()
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault();
p?.WaitForExit(1500);
return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null;
}
catch { return null; }
}
#endif
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f2b7b3e9c3e4a0f9b2a1d4c7e6f5a12
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,528 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Runtime.Serialization; // For Converters
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Handles serialization of GameObjects and Components for MCP responses.
/// Includes reflection helpers and caching for performance.
/// </summary>
public static class GameObjectSerializer
{
// --- Data Serialization ---
/// <summary>
/// Creates a serializable representation of a GameObject.
/// </summary>
public static object GetGameObjectData(GameObject go)
{
if (go == null)
return null;
return new
{
name = go.name,
instanceID = go.GetInstanceID(),
tag = go.tag,
layer = go.layer,
activeSelf = go.activeSelf,
activeInHierarchy = go.activeInHierarchy,
isStatic = go.isStatic,
scenePath = go.scene.path, // Identify which scene it belongs to
transform = new // Serialize transform components carefully to avoid JSON issues
{
// Serialize Vector3 components individually to prevent self-referencing loops.
// The default serializer can struggle with properties like Vector3.normalized.
position = new
{
x = go.transform.position.x,
y = go.transform.position.y,
z = go.transform.position.z,
},
localPosition = new
{
x = go.transform.localPosition.x,
y = go.transform.localPosition.y,
z = go.transform.localPosition.z,
},
rotation = new
{
x = go.transform.rotation.eulerAngles.x,
y = go.transform.rotation.eulerAngles.y,
z = go.transform.rotation.eulerAngles.z,
},
localRotation = new
{
x = go.transform.localRotation.eulerAngles.x,
y = go.transform.localRotation.eulerAngles.y,
z = go.transform.localRotation.eulerAngles.z,
},
scale = new
{
x = go.transform.localScale.x,
y = go.transform.localScale.y,
z = go.transform.localScale.z,
},
forward = new
{
x = go.transform.forward.x,
y = go.transform.forward.y,
z = go.transform.forward.z,
},
up = new
{
x = go.transform.up.x,
y = go.transform.up.y,
z = go.transform.up.z,
},
right = new
{
x = go.transform.right.x,
y = go.transform.right.y,
z = go.transform.right.z,
},
},
parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent
// Optionally include components, but can be large
// components = go.GetComponents<Component>().Select(c => GetComponentData(c)).ToList()
// Or just component names:
componentNames = go.GetComponents<Component>()
.Select(c => c.GetType().FullName)
.ToList(),
};
}
// --- Metadata Caching for Reflection ---
private class CachedMetadata
{
public readonly List<PropertyInfo> SerializableProperties;
public readonly List<FieldInfo> SerializableFields;
public CachedMetadata(List<PropertyInfo> properties, List<FieldInfo> fields)
{
SerializableProperties = properties;
SerializableFields = fields;
}
}
// Key becomes Tuple<Type, bool>
private static readonly Dictionary<Tuple<Type, bool>, CachedMetadata> _metadataCache = new Dictionary<Tuple<Type, bool>, CachedMetadata>();
// --- End Metadata Caching ---
/// <summary>
/// Creates a serializable representation of a Component, attempting to serialize
/// public properties and fields using reflection, with caching and control over non-public fields.
/// </summary>
// Add the flag parameter here
public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true)
{
// --- Add Early Logging ---
// Debug.Log($"[GetComponentData] Starting for component: {c?.GetType()?.FullName ?? "null"} (ID: {c?.GetInstanceID() ?? 0})");
// --- End Early Logging ---
if (c == null) return null;
Type componentType = c.GetType();
// --- Special handling for Transform to avoid reflection crashes and problematic properties ---
if (componentType == typeof(Transform))
{
Transform tr = c as Transform;
// Debug.Log($"[GetComponentData] Manually serializing Transform (ID: {tr.GetInstanceID()})");
return new Dictionary<string, object>
{
{ "typeName", componentType.FullName },
{ "instanceID", tr.GetInstanceID() },
// Manually extract known-safe properties. Avoid Quaternion 'rotation' and 'lossyScale'.
{ "position", CreateTokenFromValue(tr.position, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "localPosition", CreateTokenFromValue(tr.localPosition, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "eulerAngles", CreateTokenFromValue(tr.eulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, // Use Euler angles
{ "localEulerAngles", CreateTokenFromValue(tr.localEulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "localScale", CreateTokenFromValue(tr.localScale, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "right", CreateTokenFromValue(tr.right, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "up", CreateTokenFromValue(tr.up, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "forward", CreateTokenFromValue(tr.forward, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "parentInstanceID", tr.parent?.gameObject.GetInstanceID() ?? 0 },
{ "rootInstanceID", tr.root?.gameObject.GetInstanceID() ?? 0 },
{ "childCount", tr.childCount },
// Include standard Object/Component properties
{ "name", tr.name },
{ "tag", tr.tag },
{ "gameObjectInstanceID", tr.gameObject?.GetInstanceID() ?? 0 }
};
}
// --- End Special handling for Transform ---
// --- Special handling for Camera to avoid matrix-related crashes ---
if (componentType == typeof(Camera))
{
Camera cam = c as Camera;
var cameraProperties = new Dictionary<string, object>();
// List of safe properties to serialize
var safeProperties = new Dictionary<string, Func<object>>
{
{ "nearClipPlane", () => cam.nearClipPlane },
{ "farClipPlane", () => cam.farClipPlane },
{ "fieldOfView", () => cam.fieldOfView },
{ "renderingPath", () => (int)cam.renderingPath },
{ "actualRenderingPath", () => (int)cam.actualRenderingPath },
{ "allowHDR", () => cam.allowHDR },
{ "allowMSAA", () => cam.allowMSAA },
{ "allowDynamicResolution", () => cam.allowDynamicResolution },
{ "forceIntoRenderTexture", () => cam.forceIntoRenderTexture },
{ "orthographicSize", () => cam.orthographicSize },
{ "orthographic", () => cam.orthographic },
{ "opaqueSortMode", () => (int)cam.opaqueSortMode },
{ "transparencySortMode", () => (int)cam.transparencySortMode },
{ "depth", () => cam.depth },
{ "aspect", () => cam.aspect },
{ "cullingMask", () => cam.cullingMask },
{ "eventMask", () => cam.eventMask },
{ "backgroundColor", () => cam.backgroundColor },
{ "clearFlags", () => (int)cam.clearFlags },
{ "stereoEnabled", () => cam.stereoEnabled },
{ "stereoSeparation", () => cam.stereoSeparation },
{ "stereoConvergence", () => cam.stereoConvergence },
{ "enabled", () => cam.enabled },
{ "name", () => cam.name },
{ "tag", () => cam.tag },
{ "gameObject", () => new { name = cam.gameObject.name, instanceID = cam.gameObject.GetInstanceID() } }
};
foreach (var prop in safeProperties)
{
try
{
var value = prop.Value();
if (value != null)
{
AddSerializableValue(cameraProperties, prop.Key, value.GetType(), value);
}
}
catch (Exception)
{
// Silently skip any property that fails
continue;
}
}
return new Dictionary<string, object>
{
{ "typeName", componentType.FullName },
{ "instanceID", cam.GetInstanceID() },
{ "properties", cameraProperties }
};
}
// --- End Special handling for Camera ---
var data = new Dictionary<string, object>
{
{ "typeName", componentType.FullName },
{ "instanceID", c.GetInstanceID() }
};
// --- Get Cached or Generate Metadata (using new cache key) ---
Tuple<Type, bool> cacheKey = new Tuple<Type, bool>(componentType, includeNonPublicSerializedFields);
if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData))
{
var propertiesToCache = new List<PropertyInfo>();
var fieldsToCache = new List<FieldInfo>();
// Traverse the hierarchy from the component type up to MonoBehaviour
Type currentType = componentType;
while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object))
{
// Get properties declared only at the current type level
BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
foreach (var propInfo in currentType.GetProperties(propFlags))
{
// Basic filtering (readable, not indexer, not transform which is handled elsewhere)
if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue;
// Add if not already added (handles overrides - keep the most derived version)
if (!propertiesToCache.Any(p => p.Name == propInfo.Name))
{
propertiesToCache.Add(propInfo);
}
}
// Get fields declared only at the current type level (both public and non-public)
BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
var declaredFields = currentType.GetFields(fieldFlags);
// Process the declared Fields for caching
foreach (var fieldInfo in declaredFields)
{
if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields
// Add if not already added (handles hiding - keep the most derived version)
if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue;
bool shouldInclude = false;
if (includeNonPublicSerializedFields)
{
// If TRUE, include Public OR NonPublic with [SerializeField]
shouldInclude = fieldInfo.IsPublic || (fieldInfo.IsPrivate && fieldInfo.IsDefined(typeof(SerializeField), inherit: false));
}
else // includeNonPublicSerializedFields is FALSE
{
// If FALSE, include ONLY if it is explicitly Public.
shouldInclude = fieldInfo.IsPublic;
}
if (shouldInclude)
{
fieldsToCache.Add(fieldInfo);
}
}
// Move to the base type
currentType = currentType.BaseType;
}
// --- End Hierarchy Traversal ---
cachedData = new CachedMetadata(propertiesToCache, fieldsToCache);
_metadataCache[cacheKey] = cachedData; // Add to cache with combined key
}
// --- End Get Cached or Generate Metadata ---
// --- Use cached metadata ---
var serializablePropertiesOutput = new Dictionary<string, object>();
// --- Add Logging Before Property Loop ---
// Debug.Log($"[GetComponentData] Starting property loop for {componentType.Name}...");
// --- End Logging Before Property Loop ---
// Use cached properties
foreach (var propInfo in cachedData.SerializableProperties)
{
string propName = propInfo.Name;
// --- Skip known obsolete/problematic Component shortcut properties ---
bool skipProperty = false;
if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" ||
propName == "light" || propName == "animation" || propName == "constantForce" ||
propName == "renderer" || propName == "audio" || propName == "networkView" ||
propName == "collider" || propName == "collider2D" || propName == "hingeJoint" ||
propName == "particleSystem" ||
// Also skip potentially problematic Matrix properties prone to cycles/errors
propName == "worldToLocalMatrix" || propName == "localToWorldMatrix")
{
// Debug.Log($"[GetComponentData] Explicitly skipping generic property: {propName}"); // Optional log
skipProperty = true;
}
// --- End Skip Generic Properties ---
// --- Skip specific potentially problematic Camera properties ---
if (componentType == typeof(Camera) &&
(propName == "pixelRect" ||
propName == "rect" ||
propName == "cullingMatrix" ||
propName == "useOcclusionCulling" ||
propName == "worldToCameraMatrix" ||
propName == "projectionMatrix" ||
propName == "nonJitteredProjectionMatrix" ||
propName == "previousViewProjectionMatrix" ||
propName == "cameraToWorldMatrix"))
{
// Debug.Log($"[GetComponentData] Explicitly skipping Camera property: {propName}");
skipProperty = true;
}
// --- End Skip Camera Properties ---
// --- Skip specific potentially problematic Transform properties ---
if (componentType == typeof(Transform) &&
(propName == "lossyScale" ||
propName == "rotation" ||
propName == "worldToLocalMatrix" ||
propName == "localToWorldMatrix"))
{
// Debug.Log($"[GetComponentData] Explicitly skipping Transform property: {propName}");
skipProperty = true;
}
// --- End Skip Transform Properties ---
// Skip if flagged
if (skipProperty)
{
continue;
}
try
{
// --- Add detailed logging ---
// Debug.Log($"[GetComponentData] Accessing: {componentType.Name}.{propName}");
// --- End detailed logging ---
object value = propInfo.GetValue(c);
Type propType = propInfo.PropertyType;
AddSerializableValue(serializablePropertiesOutput, propName, propType, value);
}
catch (Exception)
{
// Debug.LogWarning($"Could not read property {propName} on {componentType.Name}");
}
}
// --- Add Logging Before Field Loop ---
// Debug.Log($"[GetComponentData] Starting field loop for {componentType.Name}...");
// --- End Logging Before Field Loop ---
// Use cached fields
foreach (var fieldInfo in cachedData.SerializableFields)
{
try
{
// --- Add detailed logging for fields ---
// Debug.Log($"[GetComponentData] Accessing Field: {componentType.Name}.{fieldInfo.Name}");
// --- End detailed logging for fields ---
object value = fieldInfo.GetValue(c);
string fieldName = fieldInfo.Name;
Type fieldType = fieldInfo.FieldType;
AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value);
}
catch (Exception)
{
// Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}");
}
}
// --- End Use cached metadata ---
if (serializablePropertiesOutput.Count > 0)
{
data["properties"] = serializablePropertiesOutput;
}
return data;
}
// Helper function to decide how to serialize different types
private static void AddSerializableValue(Dictionary<string, object> dict, string name, Type type, object value)
{
// Simplified: Directly use CreateTokenFromValue which uses the serializer
if (value == null)
{
dict[name] = null;
return;
}
try
{
// Use the helper that employs our custom serializer settings
JToken token = CreateTokenFromValue(value, type);
if (token != null) // Check if serialization succeeded in the helper
{
// Convert JToken back to a basic object structure for the dictionary
dict[name] = ConvertJTokenToPlainObject(token);
}
// If token is null, it means serialization failed and a warning was logged.
}
catch (Exception e)
{
// Catch potential errors during JToken conversion or addition to dictionary
Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping.");
}
}
// Helper to convert JToken back to basic object structure
private static object ConvertJTokenToPlainObject(JToken token)
{
if (token == null) return null;
switch (token.Type)
{
case JTokenType.Object:
var objDict = new Dictionary<string, object>();
foreach (var prop in ((JObject)token).Properties())
{
objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value);
}
return objDict;
case JTokenType.Array:
var list = new List<object>();
foreach (var item in (JArray)token)
{
list.Add(ConvertJTokenToPlainObject(item));
}
return list;
case JTokenType.Integer:
return token.ToObject<long>(); // Use long for safety
case JTokenType.Float:
return token.ToObject<double>(); // Use double for safety
case JTokenType.String:
return token.ToObject<string>();
case JTokenType.Boolean:
return token.ToObject<bool>();
case JTokenType.Date:
return token.ToObject<DateTime>();
case JTokenType.Guid:
return token.ToObject<Guid>();
case JTokenType.Uri:
return token.ToObject<Uri>();
case JTokenType.TimeSpan:
return token.ToObject<TimeSpan>();
case JTokenType.Bytes:
return token.ToObject<byte[]>();
case JTokenType.Null:
return null;
case JTokenType.Undefined:
return null; // Treat undefined as null
default:
// Fallback for simple value types not explicitly listed
if (token is JValue jValue && jValue.Value != null)
{
return jValue.Value;
}
// Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null.");
return null;
}
}
// --- Define custom JsonSerializerSettings for OUTPUT ---
private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings
{
Converters = new List<JsonConverter>
{
new Vector3Converter(),
new Vector2Converter(),
new QuaternionConverter(),
new ColorConverter(),
new RectConverter(),
new BoundsConverter(),
new UnityEngineObjectConverter() // Handles serialization of references
},
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
// ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed
};
private static readonly JsonSerializer _outputSerializer = JsonSerializer.Create(_outputSerializerSettings);
// --- End Define custom JsonSerializerSettings ---
// Helper to create JToken using the output serializer
private static JToken CreateTokenFromValue(object value, Type type)
{
if (value == null) return JValue.CreateNull();
try
{
// Use the pre-configured OUTPUT serializer instance
return JToken.FromObject(value, _outputSerializer);
}
catch (JsonSerializationException e)
{
Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field.");
return null; // Indicate serialization failure
}
catch (Exception e) // Catch other unexpected errors
{
Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field.");
return null; // Indicate serialization failure
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 64b8ff807bc9a401c82015cbafccffac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,186 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using UnityEditor;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Shared helpers for reading and writing MCP client configuration files.
/// Consolidates file atomics and server directory resolution so the editor
/// window can focus on UI concerns only.
/// </summary>
public static class McpConfigFileHelper
{
public static string ExtractDirectoryArg(string[] args)
{
if (args == null) return null;
for (int i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase))
{
return args[i + 1];
}
}
return null;
}
public static bool PathsEqual(string a, string b)
{
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
try
{
string na = Path.GetFullPath(a.Trim());
string nb = Path.GetFullPath(b.Trim());
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
}
return string.Equals(na, nb, StringComparison.Ordinal);
}
catch
{
return false;
}
}
/// <summary>
/// Resolves the server directory to use for MCP tools, preferring
/// existing config values and falling back to installed/embedded copies.
/// </summary>
public static string ResolveServerDirectory(string pythonDir, string[] existingArgs)
{
string serverSrc = ExtractDirectoryArg(existingArgs);
bool serverValid = !string.IsNullOrEmpty(serverSrc)
&& File.Exists(Path.Combine(serverSrc, "server.py"));
if (!serverValid)
{
if (!string.IsNullOrEmpty(pythonDir)
&& File.Exists(Path.Combine(pythonDir, "server.py")))
{
serverSrc = pythonDir;
}
else
{
serverSrc = ResolveServerSource();
}
}
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !string.IsNullOrEmpty(serverSrc))
{
string norm = serverSrc.Replace('\\', '/');
int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal);
if (idx >= 0)
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
string suffix = norm.Substring(idx + "/.local/share/".Length);
serverSrc = Path.Combine(home, "Library", "Application Support", suffix);
}
}
}
catch
{
// Ignore failures and fall back to the original path.
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
&& !string.IsNullOrEmpty(serverSrc)
&& serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
&& !EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false))
{
serverSrc = ServerInstaller.GetServerPath();
}
return serverSrc;
}
public static void WriteAtomicFile(string path, string contents)
{
string tmp = path + ".tmp";
string backup = path + ".backup";
bool writeDone = false;
try
{
File.WriteAllText(tmp, contents, new UTF8Encoding(false));
try
{
File.Replace(tmp, path, backup);
writeDone = true;
}
catch (FileNotFoundException)
{
File.Move(tmp, path);
writeDone = true;
}
catch (PlatformNotSupportedException)
{
if (File.Exists(path))
{
try
{
if (File.Exists(backup)) File.Delete(backup);
}
catch { }
File.Move(path, backup);
}
File.Move(tmp, path);
writeDone = true;
}
}
catch (Exception ex)
{
try
{
if (!writeDone && File.Exists(backup))
{
try { File.Copy(backup, path, true); } catch { }
}
}
catch { }
throw new Exception($"Failed to write config file '{path}': {ex.Message}", ex);
}
finally
{
try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
try { if (writeDone && File.Exists(backup)) File.Delete(backup); } catch { }
}
}
public static string ResolveServerSource()
{
try
{
string remembered = EditorPrefs.GetString("MCPForUnity.ServerSrc", string.Empty);
if (!string.IsNullOrEmpty(remembered)
&& File.Exists(Path.Combine(remembered, "server.py")))
{
return remembered;
}
ServerInstaller.EnsureServerInstalled();
string installed = ServerInstaller.GetServerPath();
if (File.Exists(Path.Combine(installed, "server.py")))
{
return installed;
}
bool useEmbedded = EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false);
if (useEmbedded
&& ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)
&& File.Exists(Path.Combine(embedded, "server.py")))
{
return embedded;
}
return installed;
}
catch
{
return ServerInstaller.GetServerPath();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f69ad468942b74c0ea24e3e8e5f21a4b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,297 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Shared helper for MCP client configuration management with sophisticated
/// logic for preserving existing configs and handling different client types
/// </summary>
public static class McpConfigurationHelper
{
private const string LOCK_CONFIG_KEY = "MCPForUnity.LockCursorConfig";
/// <summary>
/// Writes MCP configuration to the specified path using sophisticated logic
/// that preserves existing configuration and only writes when necessary
/// </summary>
public static string WriteMcpConfiguration(string pythonDir, string configPath, McpClient mcpClient = null)
{
// 0) Respect explicit lock (hidden pref or UI toggle)
try
{
if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
return "Skipped (locked)";
}
catch { }
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
// Read existing config if it exists
string existingJson = "{}";
if (File.Exists(configPath))
{
try
{
existingJson = File.ReadAllText(configPath);
}
catch (Exception e)
{
Debug.LogWarning($"Error reading existing config: {e.Message}.");
}
}
// Parse the existing JSON while preserving all properties
dynamic existingConfig;
try
{
if (string.IsNullOrWhiteSpace(existingJson))
{
existingConfig = new JObject();
}
else
{
existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new JObject();
}
}
catch
{
// If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object
if (!string.IsNullOrWhiteSpace(existingJson))
{
Debug.LogWarning("UnityMCP: Configuration file could not be parsed; rewriting server block.");
}
existingConfig = new JObject();
}
// Determine existing entry references (command/args)
string existingCommand = null;
string[] existingArgs = null;
bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode);
try
{
if (isVSCode)
{
existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString();
existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject<string[]>();
}
else
{
existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString();
existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject<string[]>();
}
}
catch { }
// 1) Start from existing, only fill gaps (prefer trusted resolver)
string uvPath = ServerInstaller.FindUvPath();
// Optionally trust existingCommand if it looks like uv/uv.exe
try
{
var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
{
uvPath = existingCommand;
}
}
catch { }
if (uvPath == null) return "UV package manager not found. Please install UV first.";
string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
// 2) Canonical args order
var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
// 3) Only write if changed
bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
|| !ArgsEqual(existingArgs, newArgs);
if (!changed)
{
return "Configured successfully"; // nothing to do
}
// 4) Ensure containers exist and write back minimal changes
JObject existingRoot;
if (existingConfig is JObject eo)
existingRoot = eo;
else
existingRoot = JObject.FromObject(existingConfig);
existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient);
string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);
McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson);
try
{
if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
}
catch { }
return "Configured successfully";
}
/// <summary>
/// Configures a Codex client with sophisticated TOML handling
/// </summary>
public static string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient)
{
try
{
if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
return "Skipped (locked)";
}
catch { }
string existingToml = string.Empty;
if (File.Exists(configPath))
{
try
{
existingToml = File.ReadAllText(configPath);
}
catch (Exception e)
{
Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}");
existingToml = string.Empty;
}
}
string existingCommand = null;
string[] existingArgs = null;
if (!string.IsNullOrWhiteSpace(existingToml))
{
CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs);
}
string uvPath = ServerInstaller.FindUvPath();
try
{
var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
{
uvPath = existingCommand;
}
}
catch { }
if (uvPath == null)
{
return "UV package manager not found. Please install UV first.";
}
string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
bool changed = true;
if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null)
{
changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
|| !ArgsEqual(existingArgs, newArgs);
}
if (!changed)
{
return "Configured successfully";
}
string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc);
string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock);
McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml);
try
{
if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
}
catch { }
return "Configured successfully";
}
/// <summary>
/// Validates UV binary by running --version command
/// </summary>
private static bool IsValidUvBinary(string path)
{
try
{
if (!File.Exists(path)) return false;
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = path,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var p = System.Diagnostics.Process.Start(psi);
if (p == null) return false;
if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; }
if (p.ExitCode != 0) return false;
string output = p.StandardOutput.ReadToEnd().Trim();
return output.StartsWith("uv ");
}
catch { return false; }
}
/// <summary>
/// Compares two string arrays for equality
/// </summary>
private static bool ArgsEqual(string[] a, string[] b)
{
if (a == null || b == null) return a == b;
if (a.Length != b.Length) return false;
for (int i = 0; i < a.Length; i++)
{
if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false;
}
return true;
}
/// <summary>
/// Gets the appropriate config file path for the given MCP client based on OS
/// </summary>
public static string GetClientConfigPath(McpClient mcpClient)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return mcpClient.windowsConfigPath;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return string.IsNullOrEmpty(mcpClient.macConfigPath)
? mcpClient.linuxConfigPath
: mcpClient.macConfigPath;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return mcpClient.linuxConfigPath;
}
else
{
return mcpClient.linuxConfigPath; // fallback
}
}
/// <summary>
/// Creates the directory for the config file if it doesn't exist
/// </summary>
public static void EnsureConfigDirectoryExists(string configPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(configPath));
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e45ac2a13b4c1ba468b8e3aa67b292ca
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,31 @@
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
internal static class McpLog
{
private const string Prefix = "<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>:";
private static bool IsDebugEnabled()
{
try { return EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); } catch { return false; }
}
public static void Info(string message, bool always = true)
{
if (!always && !IsDebugEnabled()) return;
Debug.Log($"{Prefix} {message}");
}
public static void Warn(string message)
{
Debug.LogWarning($"<color=#cc7a00>{Prefix} {message}</color>");
}
public static void Error(string message)
{
Debug.LogError($"<color=#cc3333>{Prefix} {message}</color>");
}
}
}

View File

@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 9e2c3f8a4f4f48d8a4c1b7b8e3f5a1c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,123 @@
using System;
using System.IO;
using UnityEngine;
using UnityEditor;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Shared helper for resolving Python server directory paths with support for
/// development mode, embedded servers, and installed packages
/// </summary>
public static class McpPathResolver
{
private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer";
/// <summary>
/// Resolves the Python server directory path with comprehensive logic
/// including development mode support and fallback mechanisms
/// </summary>
public static string FindPackagePythonDirectory(bool debugLogsEnabled = false)
{
string pythonDir = McpConfigFileHelper.ResolveServerSource();
try
{
// Only check dev paths if we're using a file-based package (development mode)
bool isDevelopmentMode = IsDevelopmentMode();
if (isDevelopmentMode)
{
string currentPackagePath = Path.GetDirectoryName(Application.dataPath);
string[] devPaths = {
Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"),
Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"),
};
foreach (string devPath in devPaths)
{
if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py")))
{
if (debugLogsEnabled)
{
Debug.Log($"Currently in development mode. Package: {devPath}");
}
return devPath;
}
}
}
// Resolve via shared helper (handles local registry and older fallback) only if dev override on
if (EditorPrefs.GetBool(USE_EMBEDDED_SERVER_KEY, false))
{
if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded))
{
return embedded;
}
}
// Log only if the resolved path does not actually contain server.py
if (debugLogsEnabled)
{
bool hasServer = false;
try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { }
if (!hasServer)
{
Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path");
}
}
}
catch (Exception e)
{
Debug.LogError($"Error finding package path: {e.Message}");
}
return pythonDir;
}
/// <summary>
/// Checks if the current Unity project is in development mode
/// (i.e., the package is referenced as a local file path in manifest.json)
/// </summary>
private static bool IsDevelopmentMode()
{
try
{
// Only treat as development if manifest explicitly references a local file path for the package
string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json");
if (!File.Exists(manifestPath)) return false;
string manifestContent = File.ReadAllText(manifestPath);
// Look specifically for our package dependency set to a file: URL
// This avoids auto-enabling dev mode just because a repo exists elsewhere on disk
if (manifestContent.IndexOf("\"com.coplaydev.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0)
{
int idx = manifestContent.IndexOf("com.coplaydev.unity-mcp", StringComparison.OrdinalIgnoreCase);
// Crude but effective: check for "file:" in the same line/value
if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0
&& manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
catch
{
return false;
}
}
/// <summary>
/// Gets the appropriate PATH prepend for the current platform when running external processes
/// </summary>
public static string GetPathPrepend()
{
if (Application.platform == RuntimePlatform.OSXEditor)
return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
else if (Application.platform == RuntimePlatform.LinuxEditor)
return "/usr/local/bin:/usr/bin:/bin";
return null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c76f0c7ff138ba4a952481e04bc3974
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,107 @@
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Auto-runs legacy/older install detection on package load/update (log-only).
/// Runs once per embedded server version using an EditorPrefs version-scoped key.
/// </summary>
[InitializeOnLoad]
public static class PackageDetector
{
private const string DetectOnceFlagKeyPrefix = "MCPForUnity.LegacyDetectLogged:";
static PackageDetector()
{
try
{
string pkgVer = ReadPackageVersionOrFallback();
string key = DetectOnceFlagKeyPrefix + pkgVer;
// Always force-run if legacy roots exist or canonical install is missing
bool legacyPresent = LegacyRootsExist();
bool canonicalMissing = !System.IO.File.Exists(System.IO.Path.Combine(ServerInstaller.GetServerPath(), "server.py"));
if (!EditorPrefs.GetBool(key, false) || legacyPresent || canonicalMissing)
{
// Marshal the entire flow to the main thread. EnsureServerInstalled may touch Unity APIs.
EditorApplication.delayCall += () =>
{
string error = null;
System.Exception capturedEx = null;
try
{
// Ensure any UnityEditor API usage inside runs on the main thread
ServerInstaller.EnsureServerInstalled();
}
catch (System.Exception ex)
{
error = ex.Message;
capturedEx = ex;
}
// Unity APIs must stay on main thread
try { EditorPrefs.SetBool(key, true); } catch { }
// Ensure prefs cleanup happens on main thread
try { EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); } catch { }
try { EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride"); } catch { }
if (!string.IsNullOrEmpty(error))
{
Debug.LogWarning($"MCP for Unity: Auto-detect on load failed: {capturedEx}");
// Alternatively: Debug.LogException(capturedEx);
}
};
}
}
catch { /* ignore */ }
}
private static string ReadEmbeddedVersionOrFallback()
{
try
{
if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
{
var p = System.IO.Path.Combine(embeddedSrc, "server_version.txt");
if (System.IO.File.Exists(p))
return (System.IO.File.ReadAllText(p)?.Trim() ?? "unknown");
}
}
catch { }
return "unknown";
}
private static string ReadPackageVersionOrFallback()
{
try
{
var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(PackageDetector).Assembly);
if (info != null && !string.IsNullOrEmpty(info.version)) return info.version;
}
catch { }
// Fallback to embedded server version if package info unavailable
return ReadEmbeddedVersionOrFallback();
}
private static bool LegacyRootsExist()
{
try
{
string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile) ?? string.Empty;
string[] roots =
{
System.IO.Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"),
System.IO.Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src")
};
foreach (var r in roots)
{
try { if (System.IO.File.Exists(System.IO.Path.Combine(r, "server.py"))) return true; } catch { }
}
}
catch { }
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b82eaef548d164ca095f17db64d15af8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,43 @@
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Handles automatic installation of the Python server when the package is first installed.
/// </summary>
[InitializeOnLoad]
public static class PackageInstaller
{
private const string InstallationFlagKey = "MCPForUnity.ServerInstalled";
static PackageInstaller()
{
// Check if this is the first time the package is loaded
if (!EditorPrefs.GetBool(InstallationFlagKey, false))
{
// Schedule the installation for after Unity is fully loaded
EditorApplication.delayCall += InstallServerOnFirstLoad;
}
}
private static void InstallServerOnFirstLoad()
{
try
{
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Installing Python server...");
ServerInstaller.EnsureServerInstalled();
// Mark as installed
EditorPrefs.SetBool(InstallationFlagKey, true);
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Python server installation completed successfully.");
}
catch (System.Exception ex)
{
Debug.LogError($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Failed to install Python server: {ex.Message}");
Debug.LogWarning("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: You may need to manually install the Python server. Check the MCP For Unity Window for instructions.");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 19e6eaa637484e9fa19f9a0459809de2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,319 @@
using System;
using System.IO;
using UnityEditor;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Manages dynamic port allocation and persistent storage for MCP for Unity
/// </summary>
public static class PortManager
{
private static bool IsDebugEnabled()
{
try { return EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); }
catch { return false; }
}
private const int DefaultPort = 6400;
private const int MaxPortAttempts = 100;
private const string RegistryFileName = "unity-mcp-port.json";
[Serializable]
public class PortConfig
{
public int unity_port;
public string created_date;
public string project_path;
}
/// <summary>
/// Get the port to use - either from storage or discover a new one
/// Will try stored port first, then fallback to discovering new port
/// </summary>
/// <returns>Port number to use</returns>
public static int GetPortWithFallback()
{
// Try to load stored port first, but only if it's from the current project
var storedConfig = GetStoredPortConfig();
if (storedConfig != null &&
storedConfig.unity_port > 0 &&
string.Equals(storedConfig.project_path ?? string.Empty, Application.dataPath ?? string.Empty, StringComparison.OrdinalIgnoreCase) &&
IsPortAvailable(storedConfig.unity_port))
{
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Using stored port {storedConfig.unity_port} for current project");
return storedConfig.unity_port;
}
// If stored port exists but is currently busy, wait briefly for release
if (storedConfig != null && storedConfig.unity_port > 0)
{
if (WaitForPortRelease(storedConfig.unity_port, 1500))
{
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Stored port {storedConfig.unity_port} became available after short wait");
return storedConfig.unity_port;
}
// Prefer sticking to the same port; let the caller handle bind retries/fallbacks
return storedConfig.unity_port;
}
// If no valid stored port, find a new one and save it
int newPort = FindAvailablePort();
SavePort(newPort);
return newPort;
}
/// <summary>
/// Discover and save a new available port (used by Auto-Connect button)
/// </summary>
/// <returns>New available port</returns>
public static int DiscoverNewPort()
{
int newPort = FindAvailablePort();
SavePort(newPort);
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Discovered and saved new port: {newPort}");
return newPort;
}
/// <summary>
/// Find an available port starting from the default port
/// </summary>
/// <returns>Available port number</returns>
private static int FindAvailablePort()
{
// Always try default port first
if (IsPortAvailable(DefaultPort))
{
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Using default port {DefaultPort}");
return DefaultPort;
}
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Default port {DefaultPort} is in use, searching for alternative...");
// Search for alternatives
for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++)
{
if (IsPortAvailable(port))
{
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Found available port {port}");
return port;
}
}
throw new Exception($"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}");
}
/// <summary>
/// Check if a specific port is available for binding
/// </summary>
/// <param name="port">Port to check</param>
/// <returns>True if port is available</returns>
public static bool IsPortAvailable(int port)
{
try
{
var testListener = new TcpListener(IPAddress.Loopback, port);
testListener.Start();
testListener.Stop();
return true;
}
catch (SocketException)
{
return false;
}
}
/// <summary>
/// Check if a port is currently being used by MCP for Unity
/// This helps avoid unnecessary port changes when Unity itself is using the port
/// </summary>
/// <param name="port">Port to check</param>
/// <returns>True if port appears to be used by MCP for Unity</returns>
public static bool IsPortUsedByMCPForUnity(int port)
{
try
{
// Try to make a quick connection to see if it's an MCP for Unity server
using var client = new TcpClient();
var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
if (connectTask.Wait(100)) // 100ms timeout
{
// If connection succeeded, it's likely the MCP for Unity server
return client.Connected;
}
return false;
}
catch
{
return false;
}
}
/// <summary>
/// Wait for a port to become available for a limited amount of time.
/// Used to bridge the gap during domain reload when the old listener
/// hasn't released the socket yet.
/// </summary>
private static bool WaitForPortRelease(int port, int timeoutMs)
{
int waited = 0;
const int step = 100;
while (waited < timeoutMs)
{
if (IsPortAvailable(port))
{
return true;
}
// If the port is in use by an MCP instance, continue waiting briefly
if (!IsPortUsedByMCPForUnity(port))
{
// In use by something else; don't keep waiting
return false;
}
Thread.Sleep(step);
waited += step;
}
return IsPortAvailable(port);
}
/// <summary>
/// Save port to persistent storage
/// </summary>
/// <param name="port">Port to save</param>
private static void SavePort(int port)
{
try
{
var portConfig = new PortConfig
{
unity_port = port,
created_date = DateTime.UtcNow.ToString("O"),
project_path = Application.dataPath
};
string registryDir = GetRegistryDirectory();
Directory.CreateDirectory(registryDir);
string registryFile = GetRegistryFilePath();
string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented);
// Write to hashed, project-scoped file
File.WriteAllText(registryFile, json, new System.Text.UTF8Encoding(false));
// Also write to legacy stable filename to avoid hash/case drift across reloads
string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
File.WriteAllText(legacy, json, new System.Text.UTF8Encoding(false));
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Saved port {port} to storage");
}
catch (Exception ex)
{
Debug.LogWarning($"Could not save port to storage: {ex.Message}");
}
}
/// <summary>
/// Load port from persistent storage
/// </summary>
/// <returns>Stored port number, or 0 if not found</returns>
private static int LoadStoredPort()
{
try
{
string registryFile = GetRegistryFilePath();
if (!File.Exists(registryFile))
{
// Backwards compatibility: try the legacy file name
string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
if (!File.Exists(legacy))
{
return 0;
}
registryFile = legacy;
}
string json = File.ReadAllText(registryFile);
var portConfig = JsonConvert.DeserializeObject<PortConfig>(json);
return portConfig?.unity_port ?? 0;
}
catch (Exception ex)
{
Debug.LogWarning($"Could not load port from storage: {ex.Message}");
return 0;
}
}
/// <summary>
/// Get the current stored port configuration
/// </summary>
/// <returns>Port configuration if exists, null otherwise</returns>
public static PortConfig GetStoredPortConfig()
{
try
{
string registryFile = GetRegistryFilePath();
if (!File.Exists(registryFile))
{
// Backwards compatibility: try the legacy file
string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
if (!File.Exists(legacy))
{
return null;
}
registryFile = legacy;
}
string json = File.ReadAllText(registryFile);
return JsonConvert.DeserializeObject<PortConfig>(json);
}
catch (Exception ex)
{
Debug.LogWarning($"Could not load port config: {ex.Message}");
return null;
}
}
private static string GetRegistryDirectory()
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp");
}
private static string GetRegistryFilePath()
{
string dir = GetRegistryDirectory();
string hash = ComputeProjectHash(Application.dataPath);
string fileName = $"unity-mcp-port-{hash}.json";
return Path.Combine(dir, fileName);
}
private static string ComputeProjectHash(string input)
{
try
{
using SHA1 sha1 = SHA1.Create();
byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty);
byte[] hashBytes = sha1.ComputeHash(bytes);
var sb = new StringBuilder();
foreach (byte b in hashBytes)
{
sb.Append(b.ToString("x2"));
}
return sb.ToString()[..8]; // short, sufficient for filenames
}
catch
{
return "default";
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a1b2c3d4e5f6789012345678901234ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Provides static methods for creating standardized success and error response objects.
/// Ensures consistent JSON structure for communication back to the Python server.
/// </summary>
public static class Response
{
/// <summary>
/// Creates a standardized success response object.
/// </summary>
/// <param name="message">A message describing the successful operation.</param>
/// <param name="data">Optional additional data to include in the response.</param>
/// <returns>An object representing the success response.</returns>
public static object Success(string message, object data = null)
{
if (data != null)
{
return new
{
success = true,
message = message,
data = data,
};
}
else
{
return new { success = true, message = message };
}
}
/// <summary>
/// Creates a standardized error response object.
/// </summary>
/// <param name="errorCodeOrMessage">A message describing the error.</param>
/// <param name="data">Optional additional data (e.g., error details) to include.</param>
/// <returns>An object representing the error response.</returns>
public static object Error(string errorCodeOrMessage, object data = null)
{
if (data != null)
{
// Note: The key is "error" for error messages, not "message"
return new
{
success = false,
// Preserve original behavior while adding a machine-parsable code field.
// If callers pass a code string, it will be echoed in both code and error.
code = errorCodeOrMessage,
error = errorCodeOrMessage,
data = data,
};
}
else
{
return new { success = false, code = errorCodeOrMessage, error = errorCodeOrMessage };
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 80c09a76b944f8c4691e06c4d76c4be8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,700 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
public static class ServerInstaller
{
private const string RootFolder = "UnityMCP";
private const string ServerFolder = "UnityMcpServer";
private const string VersionFileName = "server_version.txt";
/// <summary>
/// Ensures the mcp-for-unity-server is installed locally by copying from the embedded package source.
/// No network calls or Git operations are performed.
/// </summary>
public static void EnsureServerInstalled()
{
try
{
string saveLocation = GetSaveLocation();
TryCreateMacSymlinkForAppSupport();
string destRoot = Path.Combine(saveLocation, ServerFolder);
string destSrc = Path.Combine(destRoot, "src");
// Detect legacy installs and version state (logs)
DetectAndLogLegacyInstallStates(destRoot);
// Resolve embedded source and versions
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
{
throw new Exception("Could not find embedded UnityMcpServer/src in the package.");
}
string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown";
string installedVer = ReadVersionFile(Path.Combine(destSrc, VersionFileName));
bool destHasServer = File.Exists(Path.Combine(destSrc, "server.py"));
bool needOverwrite = !destHasServer
|| string.IsNullOrEmpty(installedVer)
|| (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0);
// Ensure destination exists
Directory.CreateDirectory(destRoot);
if (needOverwrite)
{
// Copy the entire UnityMcpServer folder (parent of src)
string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer
CopyDirectoryRecursive(embeddedRoot, destRoot);
// Write/refresh version file
try { File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer ?? "unknown"); } catch { }
McpLog.Info($"Installed/updated server to {destRoot} (version {embeddedVer}).");
}
// Cleanup legacy installs that are missing version or older than embedded
foreach (var legacyRoot in GetLegacyRootsForDetection())
{
try
{
string legacySrc = Path.Combine(legacyRoot, "src");
if (!File.Exists(Path.Combine(legacySrc, "server.py"))) continue;
string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName));
bool legacyOlder = string.IsNullOrEmpty(legacyVer)
|| (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0);
if (legacyOlder)
{
TryKillUvForPath(legacySrc);
try
{
Directory.Delete(legacyRoot, recursive: true);
McpLog.Info($"Removed legacy server at '{legacyRoot}'.");
}
catch (Exception ex)
{
McpLog.Warn($"Failed to remove legacy server at '{legacyRoot}': {ex.Message}");
}
}
}
catch { }
}
// Clear overrides that might point at legacy locations
try
{
EditorPrefs.DeleteKey("MCPForUnity.ServerSrc");
EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride");
}
catch { }
return;
}
catch (Exception ex)
{
// If a usable server is already present (installed or embedded), don't fail hard—just warn.
bool hasInstalled = false;
try { hasInstalled = File.Exists(Path.Combine(GetServerPath(), "server.py")); } catch { }
if (hasInstalled || TryGetEmbeddedServerSource(out _))
{
McpLog.Warn($"Using existing server; skipped install. Details: {ex.Message}");
return;
}
McpLog.Error($"Failed to ensure server installation: {ex.Message}");
}
}
public static string GetServerPath()
{
return Path.Combine(GetSaveLocation(), ServerFolder, "src");
}
/// <summary>
/// Gets the platform-specific save location for the server.
/// </summary>
private static string GetSaveLocation()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Use per-user LocalApplicationData for canonical install location
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local");
return Path.Combine(localAppData, RootFolder);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
if (string.IsNullOrEmpty(xdg))
{
xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty,
".local", "share");
}
return Path.Combine(xdg, RootFolder);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// On macOS, use LocalApplicationData (~/Library/Application Support)
var localAppSupport = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
// Unity/Mono may map LocalApplicationData to ~/.local/share on macOS; normalize to Application Support
bool looksLikeXdg = !string.IsNullOrEmpty(localAppSupport) && localAppSupport.Replace('\\', '/').Contains("/.local/share");
if (string.IsNullOrEmpty(localAppSupport) || looksLikeXdg)
{
// Fallback: construct from $HOME
var home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
localAppSupport = Path.Combine(home, "Library", "Application Support");
}
TryCreateMacSymlinkForAppSupport();
return Path.Combine(localAppSupport, RootFolder);
}
throw new Exception("Unsupported operating system.");
}
/// <summary>
/// On macOS, create a no-spaces symlink ~/Library/AppSupport -> ~/Library/Application Support
/// to mitigate arg parsing and quoting issues in some MCP clients.
/// Safe to call repeatedly.
/// </summary>
private static void TryCreateMacSymlinkForAppSupport()
{
try
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return;
string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
if (string.IsNullOrEmpty(home)) return;
string canonical = Path.Combine(home, "Library", "Application Support");
string symlink = Path.Combine(home, "Library", "AppSupport");
// If symlink exists already, nothing to do
if (Directory.Exists(symlink) || File.Exists(symlink)) return;
// Create symlink only if canonical exists
if (!Directory.Exists(canonical)) return;
// Use 'ln -s' to create a directory symlink (macOS)
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "/bin/ln",
Arguments = $"-s \"{canonical}\" \"{symlink}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var p = System.Diagnostics.Process.Start(psi);
p?.WaitForExit(2000);
}
catch { /* best-effort */ }
}
private static bool IsDirectoryWritable(string path)
{
try
{
File.Create(Path.Combine(path, "test.txt")).Dispose();
File.Delete(Path.Combine(path, "test.txt"));
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Checks if the server is installed at the specified location.
/// </summary>
private static bool IsServerInstalled(string location)
{
return Directory.Exists(location)
&& File.Exists(Path.Combine(location, ServerFolder, "src", "server.py"));
}
/// <summary>
/// Detects legacy installs or older versions and logs findings (no deletion yet).
/// </summary>
private static void DetectAndLogLegacyInstallStates(string canonicalRoot)
{
try
{
string canonicalSrc = Path.Combine(canonicalRoot, "src");
// Normalize canonical root for comparisons
string normCanonicalRoot = NormalizePathSafe(canonicalRoot);
string embeddedSrc = null;
TryGetEmbeddedServerSource(out embeddedSrc);
string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc ?? string.Empty, VersionFileName));
string installedVer = ReadVersionFile(Path.Combine(canonicalSrc, VersionFileName));
// Legacy paths (macOS/Linux .config; Windows roaming as example)
foreach (var legacyRoot in GetLegacyRootsForDetection())
{
// Skip logging for the canonical root itself
if (PathsEqualSafe(legacyRoot, normCanonicalRoot))
continue;
string legacySrc = Path.Combine(legacyRoot, "src");
bool hasServer = File.Exists(Path.Combine(legacySrc, "server.py"));
string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName));
if (hasServer)
{
// Case 1: No version file
if (string.IsNullOrEmpty(legacyVer))
{
McpLog.Info("Detected legacy install without version file at: " + legacyRoot, always: false);
}
// Case 2: Lives in legacy path
McpLog.Info("Detected legacy install path: " + legacyRoot, always: false);
// Case 3: Has version but appears older than embedded
if (!string.IsNullOrEmpty(embeddedVer) && !string.IsNullOrEmpty(legacyVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0)
{
McpLog.Info($"Legacy install version {legacyVer} is older than embedded {embeddedVer}", always: false);
}
}
}
// Also log if canonical is missing version (treated as older)
if (Directory.Exists(canonicalRoot))
{
if (string.IsNullOrEmpty(installedVer))
{
McpLog.Info("Canonical install missing version file (treat as older). Path: " + canonicalRoot, always: false);
}
else if (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0)
{
McpLog.Info($"Canonical install version {installedVer} is older than embedded {embeddedVer}", always: false);
}
}
}
catch (Exception ex)
{
McpLog.Warn("Detect legacy/version state failed: " + ex.Message);
}
}
private static string NormalizePathSafe(string path)
{
try { return string.IsNullOrEmpty(path) ? path : Path.GetFullPath(path.Trim()); }
catch { return path; }
}
private static bool PathsEqualSafe(string a, string b)
{
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
string na = NormalizePathSafe(a);
string nb = NormalizePathSafe(b);
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
}
return string.Equals(na, nb, StringComparison.Ordinal);
}
catch { return false; }
}
private static IEnumerable<string> GetLegacyRootsForDetection()
{
var roots = new System.Collections.Generic.List<string>();
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
// macOS/Linux legacy
roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer"));
roots.Add(Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer"));
// Windows roaming example
try
{
string roaming = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
if (!string.IsNullOrEmpty(roaming))
roots.Add(Path.Combine(roaming, "UnityMCP", "UnityMcpServer"));
// Windows legacy: early installers/dev scripts used %LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer
// Detect this location so we can clean up older copies during install/update.
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
if (!string.IsNullOrEmpty(localAppData))
roots.Add(Path.Combine(localAppData, "Programs", "UnityMCP", "UnityMcpServer"));
}
catch { }
return roots;
}
private static void TryKillUvForPath(string serverSrcPath)
{
try
{
if (string.IsNullOrEmpty(serverSrcPath)) return;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "/usr/bin/pgrep",
Arguments = $"-f \"uv .*--directory {serverSrcPath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var p = System.Diagnostics.Process.Start(psi);
if (p == null) return;
string outp = p.StandardOutput.ReadToEnd();
p.WaitForExit(1500);
if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp))
{
foreach (var line in outp.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
{
if (int.TryParse(line.Trim(), out int pid))
{
try { System.Diagnostics.Process.GetProcessById(pid).Kill(); } catch { }
}
}
}
}
catch { }
}
private static string ReadVersionFile(string path)
{
try
{
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null;
string v = File.ReadAllText(path).Trim();
return string.IsNullOrEmpty(v) ? null : v;
}
catch { return null; }
}
private static int CompareSemverSafe(string a, string b)
{
try
{
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return 0;
var ap = a.Split('.');
var bp = b.Split('.');
for (int i = 0; i < Math.Max(ap.Length, bp.Length); i++)
{
int ai = (i < ap.Length && int.TryParse(ap[i], out var t1)) ? t1 : 0;
int bi = (i < bp.Length && int.TryParse(bp[i], out var t2)) ? t2 : 0;
if (ai != bi) return ai.CompareTo(bi);
}
return 0;
}
catch { return 0; }
}
/// <summary>
/// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
/// or common development locations.
/// </summary>
private static bool TryGetEmbeddedServerSource(out string srcPath)
{
return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath);
}
private static readonly string[] _skipDirs = { ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".git" };
private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
{
Directory.CreateDirectory(destinationDir);
foreach (string filePath in Directory.GetFiles(sourceDir))
{
string fileName = Path.GetFileName(filePath);
string destFile = Path.Combine(destinationDir, fileName);
File.Copy(filePath, destFile, overwrite: true);
}
foreach (string dirPath in Directory.GetDirectories(sourceDir))
{
string dirName = Path.GetFileName(dirPath);
foreach (var skip in _skipDirs)
{
if (dirName.Equals(skip, StringComparison.OrdinalIgnoreCase))
goto NextDir;
}
try { if ((File.GetAttributes(dirPath) & FileAttributes.ReparsePoint) != 0) continue; } catch { }
string destSubDir = Path.Combine(destinationDir, dirName);
CopyDirectoryRecursive(dirPath, destSubDir);
NextDir:;
}
}
public static bool RebuildMcpServer()
{
try
{
// Find embedded source
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
{
Debug.LogError("RebuildMcpServer: Could not find embedded server source.");
return false;
}
string saveLocation = GetSaveLocation();
string destRoot = Path.Combine(saveLocation, ServerFolder);
string destSrc = Path.Combine(destRoot, "src");
// Kill any running uv processes for this server
TryKillUvForPath(destSrc);
// Delete the entire installed server directory
if (Directory.Exists(destRoot))
{
try
{
Directory.Delete(destRoot, recursive: true);
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Deleted existing server at {destRoot}");
}
catch (Exception ex)
{
Debug.LogError($"Failed to delete existing server: {ex.Message}");
return false;
}
}
// Re-copy from embedded source
string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc;
Directory.CreateDirectory(destRoot);
CopyDirectoryRecursive(embeddedRoot, destRoot);
// Write version file
string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown";
try
{
File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer);
}
catch (Exception ex)
{
Debug.LogWarning($"Failed to write version file: {ex.Message}");
}
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Server rebuilt successfully at {destRoot} (version {embeddedVer})");
return true;
}
catch (Exception ex)
{
Debug.LogError($"RebuildMcpServer failed: {ex.Message}");
return false;
}
}
internal static string FindUvPath()
{
// Allow user override via EditorPrefs
try
{
string overridePath = EditorPrefs.GetString("MCPForUnity.UvPath", string.Empty);
if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
{
if (ValidateUvBinary(overridePath)) return overridePath;
}
}
catch { }
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
// Platform-specific candidate lists
string[] candidates;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty;
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
// Fast path: resolve from PATH first
try
{
var wherePsi = new System.Diagnostics.ProcessStartInfo
{
FileName = "where",
Arguments = "uv.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var wp = System.Diagnostics.Process.Start(wherePsi);
string output = wp.StandardOutput.ReadToEnd().Trim();
wp.WaitForExit(1500);
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
{
foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
string path = line.Trim();
if (File.Exists(path) && ValidateUvBinary(path)) return path;
}
}
}
catch { }
// Windows Store (PythonSoftwareFoundation) install location probe
// Example: %LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.3.13_*\LocalCache\local-packages\Python313\Scripts\uv.exe
try
{
string pkgsRoot = Path.Combine(localAppData, "Packages");
if (Directory.Exists(pkgsRoot))
{
var pythonPkgs = Directory.GetDirectories(pkgsRoot, "PythonSoftwareFoundation.Python.*", SearchOption.TopDirectoryOnly)
.OrderByDescending(p => p, StringComparer.OrdinalIgnoreCase);
foreach (var pkg in pythonPkgs)
{
string localCache = Path.Combine(pkg, "LocalCache", "local-packages");
if (!Directory.Exists(localCache)) continue;
var pyRoots = Directory.GetDirectories(localCache, "Python*", SearchOption.TopDirectoryOnly)
.OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase);
foreach (var pyRoot in pyRoots)
{
string uvExe = Path.Combine(pyRoot, "Scripts", "uv.exe");
if (File.Exists(uvExe) && ValidateUvBinary(uvExe)) return uvExe;
}
}
}
}
catch { }
candidates = new[]
{
// Preferred: WinGet Links shims (stable entrypoints)
// Per-user shim (LOCALAPPDATA) → machine-wide shim (Program Files\WinGet\Links)
Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"),
Path.Combine(programFiles, "WinGet", "Links", "uv.exe"),
// Common per-user installs
Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"),
Path.Combine(localAppData, @"Programs\Python\Python312\Scripts\uv.exe"),
Path.Combine(localAppData, @"Programs\Python\Python311\Scripts\uv.exe"),
Path.Combine(localAppData, @"Programs\Python\Python310\Scripts\uv.exe"),
Path.Combine(appData, @"Python\Python313\Scripts\uv.exe"),
Path.Combine(appData, @"Python\Python312\Scripts\uv.exe"),
Path.Combine(appData, @"Python\Python311\Scripts\uv.exe"),
Path.Combine(appData, @"Python\Python310\Scripts\uv.exe"),
// Program Files style installs (if a native installer was used)
Path.Combine(programFiles, @"uv\uv.exe"),
// Try simple name resolution later via PATH
"uv.exe",
"uv"
};
}
else
{
candidates = new[]
{
"/opt/homebrew/bin/uv",
"/usr/local/bin/uv",
"/usr/bin/uv",
"/opt/local/bin/uv",
Path.Combine(home, ".local", "bin", "uv"),
"/opt/homebrew/opt/uv/bin/uv",
// Framework Python installs
"/Library/Frameworks/Python.framework/Versions/3.13/bin/uv",
"/Library/Frameworks/Python.framework/Versions/3.12/bin/uv",
// Fallback to PATH resolution by name
"uv"
};
}
foreach (string c in candidates)
{
try
{
if (File.Exists(c) && ValidateUvBinary(c)) return c;
}
catch { /* ignore */ }
}
// Use platform-appropriate which/where to resolve from PATH (non-Windows handled here; Windows tried earlier)
try
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var whichPsi = new System.Diagnostics.ProcessStartInfo
{
FileName = "/usr/bin/which",
Arguments = "uv",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
try
{
// Prepend common user-local and package manager locations so 'which' can see them in Unity's GUI env
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
string prepend = string.Join(":", new[]
{
System.IO.Path.Combine(homeDir, ".local", "bin"),
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
"/bin"
});
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath);
}
catch { }
using var wp = System.Diagnostics.Process.Start(whichPsi);
string output = wp.StandardOutput.ReadToEnd().Trim();
wp.WaitForExit(3000);
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
{
if (ValidateUvBinary(output)) return output;
}
}
}
catch { }
// Manual PATH scan
try
{
string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
string[] parts = pathEnv.Split(Path.PathSeparator);
foreach (string part in parts)
{
try
{
// Check both uv and uv.exe
string candidateUv = Path.Combine(part, "uv");
string candidateUvExe = Path.Combine(part, "uv.exe");
if (File.Exists(candidateUv) && ValidateUvBinary(candidateUv)) return candidateUv;
if (File.Exists(candidateUvExe) && ValidateUvBinary(candidateUvExe)) return candidateUvExe;
}
catch { }
}
}
catch { }
return null;
}
private static bool ValidateUvBinary(string uvPath)
{
try
{
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = uvPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var p = System.Diagnostics.Process.Start(psi);
if (!p.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; }
if (p.ExitCode == 0)
{
string output = p.StandardOutput.ReadToEnd().Trim();
return output.StartsWith("uv ");
}
}
catch { }
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5862c6a6d0a914f4d83224f8d039cf7b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,141 @@
using System;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
public static class ServerPathResolver
{
/// <summary>
/// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
/// or common development locations. Returns true if found and sets srcPath to the folder
/// containing server.py.
/// </summary>
public static bool TryFindEmbeddedServerSource(out string srcPath)
{
// 1) Repo development layouts commonly used alongside this package
try
{
string projectRoot = Path.GetDirectoryName(Application.dataPath);
string[] devCandidates =
{
Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"),
Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"),
};
foreach (string candidate in devCandidates)
{
string full = Path.GetFullPath(candidate);
if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py")))
{
srcPath = full;
return true;
}
}
}
catch { /* ignore */ }
// 2) Resolve via local package info (no network). Fall back to Client.List on older editors.
try
{
#if UNITY_2021_2_OR_NEWER
// Primary: the package that owns this assembly
var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly);
if (owner != null)
{
if (TryResolveWithinPackage(owner, out srcPath))
{
return true;
}
}
// Secondary: scan all registered packages locally
foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages())
{
if (TryResolveWithinPackage(p, out srcPath))
{
return true;
}
}
#else
// Older Unity versions: use Package Manager Client.List as a fallback
var list = UnityEditor.PackageManager.Client.List();
while (!list.IsCompleted) { }
if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
{
foreach (var pkg in list.Result)
{
if (TryResolveWithinPackage(pkg, out srcPath))
{
return true;
}
}
}
#endif
}
catch { /* ignore */ }
// 3) Fallback to previous common install locations
try
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
string[] candidates =
{
Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"),
Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"),
};
foreach (string candidate in candidates)
{
if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py")))
{
srcPath = candidate;
return true;
}
}
}
catch { /* ignore */ }
srcPath = null;
return false;
}
private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath)
{
const string CurrentId = "com.coplaydev.unity-mcp";
srcPath = null;
if (p == null || p.name != CurrentId)
{
return false;
}
string packagePath = p.resolvedPath;
// Preferred tilde folder (embedded but excluded from import)
string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src");
if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py")))
{
srcPath = embeddedTilde;
return true;
}
// Legacy non-tilde folder
string embedded = Path.Combine(packagePath, "UnityMcpServer", "src");
if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py")))
{
srcPath = embedded;
return true;
}
// Dev-linked sibling of the package folder
string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src");
if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py")))
{
srcPath = sibling;
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Unity Bridge telemetry helper for collecting usage analytics
/// Following privacy-first approach with easy opt-out mechanisms
/// </summary>
public static class TelemetryHelper
{
private const string TELEMETRY_DISABLED_KEY = "MCPForUnity.TelemetryDisabled";
private const string CUSTOMER_UUID_KEY = "MCPForUnity.CustomerUUID";
private static Action<Dictionary<string, object>> s_sender;
/// <summary>
/// Check if telemetry is enabled (can be disabled via Environment Variable or EditorPrefs)
/// </summary>
public static bool IsEnabled
{
get
{
// Check environment variables first
var envDisable = Environment.GetEnvironmentVariable("DISABLE_TELEMETRY");
if (!string.IsNullOrEmpty(envDisable) &&
(envDisable.ToLower() == "true" || envDisable == "1"))
{
return false;
}
var unityMcpDisable = Environment.GetEnvironmentVariable("UNITY_MCP_DISABLE_TELEMETRY");
if (!string.IsNullOrEmpty(unityMcpDisable) &&
(unityMcpDisable.ToLower() == "true" || unityMcpDisable == "1"))
{
return false;
}
// Honor protocol-wide opt-out as well
var mcpDisable = Environment.GetEnvironmentVariable("MCP_DISABLE_TELEMETRY");
if (!string.IsNullOrEmpty(mcpDisable) &&
(mcpDisable.Equals("true", StringComparison.OrdinalIgnoreCase) || mcpDisable == "1"))
{
return false;
}
// Check EditorPrefs
return !UnityEditor.EditorPrefs.GetBool(TELEMETRY_DISABLED_KEY, false);
}
}
/// <summary>
/// Get or generate customer UUID for anonymous tracking
/// </summary>
public static string GetCustomerUUID()
{
var uuid = UnityEditor.EditorPrefs.GetString(CUSTOMER_UUID_KEY, "");
if (string.IsNullOrEmpty(uuid))
{
uuid = System.Guid.NewGuid().ToString();
UnityEditor.EditorPrefs.SetString(CUSTOMER_UUID_KEY, uuid);
}
return uuid;
}
/// <summary>
/// Disable telemetry (stored in EditorPrefs)
/// </summary>
public static void DisableTelemetry()
{
UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, true);
}
/// <summary>
/// Enable telemetry (stored in EditorPrefs)
/// </summary>
public static void EnableTelemetry()
{
UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, false);
}
/// <summary>
/// Send telemetry data to Python server for processing
/// This is a lightweight bridge - the actual telemetry logic is in Python
/// </summary>
public static void RecordEvent(string eventType, Dictionary<string, object> data = null)
{
if (!IsEnabled)
return;
try
{
var telemetryData = new Dictionary<string, object>
{
["event_type"] = eventType,
["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
["customer_uuid"] = GetCustomerUUID(),
["unity_version"] = Application.unityVersion,
["platform"] = Application.platform.ToString(),
["source"] = "unity_bridge"
};
if (data != null)
{
telemetryData["data"] = data;
}
// Send to Python server via existing bridge communication
// The Python server will handle actual telemetry transmission
SendTelemetryToPythonServer(telemetryData);
}
catch (Exception e)
{
// Never let telemetry errors interfere with functionality
if (IsDebugEnabled())
{
Debug.LogWarning($"Telemetry error (non-blocking): {e.Message}");
}
}
}
/// <summary>
/// Allows the bridge to register a concrete sender for telemetry payloads.
/// </summary>
public static void RegisterTelemetrySender(Action<Dictionary<string, object>> sender)
{
Interlocked.Exchange(ref s_sender, sender);
}
public static void UnregisterTelemetrySender()
{
Interlocked.Exchange(ref s_sender, null);
}
/// <summary>
/// Record bridge startup event
/// </summary>
public static void RecordBridgeStartup()
{
RecordEvent("bridge_startup", new Dictionary<string, object>
{
["bridge_version"] = "3.0.2",
["auto_connect"] = MCPForUnityBridge.IsAutoConnectMode()
});
}
/// <summary>
/// Record bridge connection event
/// </summary>
public static void RecordBridgeConnection(bool success, string error = null)
{
var data = new Dictionary<string, object>
{
["success"] = success
};
if (!string.IsNullOrEmpty(error))
{
data["error"] = error.Substring(0, Math.Min(200, error.Length));
}
RecordEvent("bridge_connection", data);
}
/// <summary>
/// Record tool execution from Unity side
/// </summary>
public static void RecordToolExecution(string toolName, bool success, float durationMs, string error = null)
{
var data = new Dictionary<string, object>
{
["tool_name"] = toolName,
["success"] = success,
["duration_ms"] = Math.Round(durationMs, 2)
};
if (!string.IsNullOrEmpty(error))
{
data["error"] = error.Substring(0, Math.Min(200, error.Length));
}
RecordEvent("tool_execution_unity", data);
}
private static void SendTelemetryToPythonServer(Dictionary<string, object> telemetryData)
{
var sender = Volatile.Read(ref s_sender);
if (sender != null)
{
try
{
sender(telemetryData);
return;
}
catch (Exception e)
{
if (IsDebugEnabled())
{
Debug.LogWarning($"Telemetry sender error (non-blocking): {e.Message}");
}
}
}
// Fallback: log when debug is enabled
if (IsDebugEnabled())
{
Debug.Log($"<b><color=#2EA3FF>MCP-TELEMETRY</color></b>: {telemetryData["event_type"]}");
}
}
private static bool IsDebugEnabled()
{
try
{
return UnityEditor.EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
}
catch
{
return false;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b8f3c2d1e7a94f6c8a9b5e3d2c1a0f9e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,24 @@
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Helper class for Vector3 operations
/// </summary>
public static class Vector3Helper
{
/// <summary>
/// Parses a JArray into a Vector3
/// </summary>
/// <param name="array">The array containing x, y, z coordinates</param>
/// <returns>A Vector3 with the parsed coordinates</returns>
/// <exception cref="System.Exception">Thrown when array is invalid</exception>
public static Vector3 ParseVector3(JArray array)
{
if (array == null || array.Count != 3)
throw new System.Exception("Vector3 must be an array of 3 floats [x, y, z].");
return new Vector3((float)array[0], (float)array[1], (float)array[2]);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f8514fd42f23cb641a36e52550825b35
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,19 @@
{
"name": "MCPForUnity.Editor",
"rootNamespace": "MCPForUnity.Editor",
"references": [
"MCPForUnity.Runtime",
"GUID:560b04d1a97f54a46a2660c3cc343a6f"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 98f702da6ca044be59a864a9419c4eab
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96dc847eb7f7a45e0b91241db934a4be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 16d3ab36890b6c14f9afeabee30e03e3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,21 @@
using Newtonsoft.Json.Linq;
namespace MCPForUnity.Editor.Models
{
/// <summary>
/// Represents a command received from the MCP client
/// </summary>
public class Command
{
/// <summary>
/// The type of command to execute
/// </summary>
public string type { get; set; }
/// <summary>
/// The parameters for the command
/// </summary>
public JObject @params { get; set; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6754c84e5deb74749bc3a19e0c9aa280
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,19 @@
using System;
using Newtonsoft.Json;
namespace MCPForUnity.Editor.Models
{
[Serializable]
public class McpConfigServer
{
[JsonProperty("command")]
public string command;
[JsonProperty("args")]
public string[] args;
// VSCode expects a transport type; include only when explicitly set
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public string type;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5fae9d995f514e9498e9613e2cdbeca9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,12 @@
using System;
using Newtonsoft.Json;
namespace MCPForUnity.Editor.Models
{
[Serializable]
public class McpConfigServers
{
[JsonProperty("unityMCP")]
public McpConfigServer unityMCP;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bcb583553e8173b49be71a5c43bd9502
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,47 @@
namespace MCPForUnity.Editor.Models
{
public class McpClient
{
public string name;
public string windowsConfigPath;
public string macConfigPath;
public string linuxConfigPath;
public McpTypes mcpType;
public string configStatus;
public McpStatus status = McpStatus.NotConfigured;
// Helper method to convert the enum to a display string
public string GetStatusDisplayString()
{
return status switch
{
McpStatus.NotConfigured => "Not Configured",
McpStatus.Configured => "Configured",
McpStatus.Running => "Running",
McpStatus.Connected => "Connected",
McpStatus.IncorrectPath => "Incorrect Path",
McpStatus.CommunicationError => "Communication Error",
McpStatus.NoResponse => "No Response",
McpStatus.UnsupportedOS => "Unsupported OS",
McpStatus.MissingConfig => "Missing MCPForUnity Config",
McpStatus.Error => configStatus.StartsWith("Error:") ? configStatus : "Error",
_ => "Unknown",
};
}
// Helper method to set both status enum and string for backward compatibility
public void SetStatus(McpStatus newStatus, string errorDetails = null)
{
status = newStatus;
if (newStatus == McpStatus.Error && !string.IsNullOrEmpty(errorDetails))
{
configStatus = $"Error: {errorDetails}";
}
else
{
configStatus = GetStatusDisplayString();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b1afa56984aec0d41808edcebf805e6a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,12 @@
using System;
using Newtonsoft.Json;
namespace MCPForUnity.Editor.Models
{
[Serializable]
public class McpConfig
{
[JsonProperty("mcpServers")]
public McpConfigServers mcpServers;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c17c09908f0c1524daa8b6957ce1f7f5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
namespace MCPForUnity.Editor.Models
{
// Enum representing the various status states for MCP clients
public enum McpStatus
{
NotConfigured, // Not set up yet
Configured, // Successfully configured
Running, // Service is running
Connected, // Successfully connected
IncorrectPath, // Configuration has incorrect paths
CommunicationError, // Connected but communication issues
NoResponse, // Connected but not responding
MissingConfig, // Config file exists but missing required elements
UnsupportedOS, // OS is not supported
Error, // General error state
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aa63057c9e5282d4887352578bf49971
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,13 @@
namespace MCPForUnity.Editor.Models
{
public enum McpTypes
{
ClaudeCode,
ClaudeDesktop,
Codex,
Cursor,
Kiro,
VSCode,
Windsurf,
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,36 @@
using System;
using Newtonsoft.Json;
namespace MCPForUnity.Editor.Models
{
[Serializable]
public class ServerConfig
{
[JsonProperty("unity_host")]
public string unityHost = "localhost";
[JsonProperty("unity_port")]
public int unityPort;
[JsonProperty("mcp_port")]
public int mcpPort;
[JsonProperty("connection_timeout")]
public float connectionTimeout;
[JsonProperty("buffer_size")]
public int bufferSize;
[JsonProperty("log_level")]
public string logLevel;
[JsonProperty("log_format")]
public string logFormat;
[JsonProperty("max_retries")]
public int maxRetries;
[JsonProperty("retry_delay")]
public float retryDelay;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e4e45386fcc282249907c2e3c7e5d9c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 600c9cb20c329d761bfa799158a87bac
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,150 @@
using System;
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Windows;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Setup
{
/// <summary>
/// Handles automatic triggering of the setup wizard
/// </summary>
[InitializeOnLoad]
public static class SetupWizard
{
private const string SETUP_COMPLETED_KEY = "MCPForUnity.SetupCompleted";
private const string SETUP_DISMISSED_KEY = "MCPForUnity.SetupDismissed";
private static bool _hasCheckedThisSession = false;
static SetupWizard()
{
// Skip in batch mode
if (Application.isBatchMode)
return;
// Show setup wizard on package import
EditorApplication.delayCall += CheckSetupNeeded;
}
/// <summary>
/// Check if setup wizard should be shown
/// </summary>
private static void CheckSetupNeeded()
{
if (_hasCheckedThisSession)
return;
_hasCheckedThisSession = true;
try
{
// Check if setup was already completed or dismissed in previous sessions
bool setupCompleted = EditorPrefs.GetBool(SETUP_COMPLETED_KEY, false);
bool setupDismissed = EditorPrefs.GetBool(SETUP_DISMISSED_KEY, false);
// Only show setup wizard if it hasn't been completed or dismissed before
if (!(setupCompleted || setupDismissed))
{
McpLog.Info("Package imported - showing setup wizard", always: false);
var dependencyResult = DependencyManager.CheckAllDependencies();
EditorApplication.delayCall += () => ShowSetupWizard(dependencyResult);
}
else
{
McpLog.Info("Setup wizard skipped - previously completed or dismissed", always: false);
}
}
catch (Exception ex)
{
McpLog.Error($"Error checking setup status: {ex.Message}");
}
}
/// <summary>
/// Show the setup wizard window
/// </summary>
public static void ShowSetupWizard(DependencyCheckResult dependencyResult = null)
{
try
{
dependencyResult ??= DependencyManager.CheckAllDependencies();
SetupWizardWindow.ShowWindow(dependencyResult);
}
catch (Exception ex)
{
McpLog.Error($"Error showing setup wizard: {ex.Message}");
}
}
/// <summary>
/// Mark setup as completed
/// </summary>
public static void MarkSetupCompleted()
{
EditorPrefs.SetBool(SETUP_COMPLETED_KEY, true);
McpLog.Info("Setup marked as completed");
}
/// <summary>
/// Mark setup as dismissed
/// </summary>
public static void MarkSetupDismissed()
{
EditorPrefs.SetBool(SETUP_DISMISSED_KEY, true);
McpLog.Info("Setup marked as dismissed");
}
/// <summary>
/// Force show setup wizard (for manual invocation)
/// </summary>
[MenuItem("Window/MCP For Unity/Setup Wizard", priority = 1)]
public static void ShowSetupWizardManual()
{
ShowSetupWizard();
}
/// <summary>
/// Check dependencies and show status
/// </summary>
[MenuItem("Window/MCP For Unity/Check Dependencies", priority = 3)]
public static void CheckDependencies()
{
var result = DependencyManager.CheckAllDependencies();
if (!result.IsSystemReady)
{
bool showWizard = EditorUtility.DisplayDialog(
"MCP for Unity - Dependencies",
$"System Status: {result.Summary}\n\nWould you like to open the Setup Wizard?",
"Open Setup Wizard",
"Close"
);
if (showWizard)
{
ShowSetupWizard(result);
}
}
else
{
EditorUtility.DisplayDialog(
"MCP for Unity - Dependencies",
"✓ All dependencies are available and ready!\n\nMCP for Unity is ready to use.",
"OK"
);
}
}
/// <summary>
/// Open MCP Client Configuration window
/// </summary>
[MenuItem("Window/MCP For Unity/Open MCP Window", priority = 4)]
public static void OpenClientConfiguration()
{
Windows.MCPForUnityEditorWindow.ShowWindow();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 345678901234abcdef0123456789abcd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,726 @@
using System;
using System.Linq;
using MCPForUnity.Editor.Data;
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Setup
{
/// <summary>
/// Setup wizard window for guiding users through dependency installation
/// </summary>
public class SetupWizardWindow : EditorWindow
{
private DependencyCheckResult _dependencyResult;
private Vector2 _scrollPosition;
private int _currentStep = 0;
private McpClients _mcpClients;
private int _selectedClientIndex = 0;
private readonly string[] _stepTitles = {
"Setup",
"Configure",
"Complete"
};
public static void ShowWindow(DependencyCheckResult dependencyResult = null)
{
var window = GetWindow<SetupWizardWindow>("MCP for Unity Setup");
window.minSize = new Vector2(500, 400);
window.maxSize = new Vector2(800, 600);
window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies();
window.Show();
}
private void OnEnable()
{
if (_dependencyResult == null)
{
_dependencyResult = DependencyManager.CheckAllDependencies();
}
_mcpClients = new McpClients();
// Check client configurations on startup
foreach (var client in _mcpClients.clients)
{
CheckClientConfiguration(client);
}
}
private void OnGUI()
{
DrawHeader();
DrawProgressBar();
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
switch (_currentStep)
{
case 0: DrawSetupStep(); break;
case 1: DrawConfigureStep(); break;
case 2: DrawCompleteStep(); break;
}
EditorGUILayout.EndScrollView();
DrawFooter();
}
private void DrawHeader()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.Label("MCP for Unity Setup Wizard", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
GUILayout.Label($"Step {_currentStep + 1} of {_stepTitles.Length}");
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// Step title
var titleStyle = new GUIStyle(EditorStyles.largeLabel)
{
fontSize = 16,
fontStyle = FontStyle.Bold
};
EditorGUILayout.LabelField(_stepTitles[_currentStep], titleStyle);
EditorGUILayout.Space();
}
private void DrawProgressBar()
{
var rect = EditorGUILayout.GetControlRect(false, 4);
var progress = (_currentStep + 1) / (float)_stepTitles.Length;
EditorGUI.ProgressBar(rect, progress, "");
EditorGUILayout.Space();
}
private void DrawSetupStep()
{
// Welcome section
DrawSectionTitle("MCP for Unity Setup");
EditorGUILayout.LabelField(
"This wizard will help you set up MCP for Unity to connect AI assistants with your Unity Editor.",
EditorStyles.wordWrappedLabel
);
EditorGUILayout.Space();
// Dependency check section
EditorGUILayout.BeginHorizontal();
DrawSectionTitle("System Check", 14);
GUILayout.FlexibleSpace();
if (GUILayout.Button("Refresh", GUILayout.Width(60), GUILayout.Height(20)))
{
_dependencyResult = DependencyManager.CheckAllDependencies();
}
EditorGUILayout.EndHorizontal();
// Show simplified dependency status
foreach (var dep in _dependencyResult.Dependencies)
{
DrawSimpleDependencyStatus(dep);
}
// Overall status and installation guidance
EditorGUILayout.Space();
if (!_dependencyResult.IsSystemReady)
{
// Only show critical warnings when dependencies are actually missing
EditorGUILayout.HelpBox(
"⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.",
MessageType.Warning
);
EditorGUILayout.Space();
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
DrawErrorStatus("Installation Required");
var recommendations = DependencyManager.GetInstallationRecommendations();
EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel);
EditorGUILayout.Space();
if (GUILayout.Button("Open Installation Links", GUILayout.Height(25)))
{
OpenInstallationUrls();
}
EditorGUILayout.EndVertical();
}
else
{
DrawSuccessStatus("System Ready");
EditorGUILayout.LabelField("All requirements are met. You can proceed to configure your AI clients.", EditorStyles.wordWrappedLabel);
}
}
private void DrawCompleteStep()
{
DrawSectionTitle("Setup Complete");
// Refresh dependency check with caching to avoid heavy operations on every repaint
if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2)
{
_dependencyResult = DependencyManager.CheckAllDependencies();
}
if (_dependencyResult.IsSystemReady)
{
DrawSuccessStatus("MCP for Unity Ready!");
EditorGUILayout.HelpBox(
"🎉 MCP for Unity is now set up and ready to use!\n\n" +
"• Dependencies verified\n" +
"• MCP server ready\n" +
"• Client configuration accessible",
MessageType.Info
);
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Documentation", GUILayout.Height(30)))
{
Application.OpenURL("https://github.com/CoplayDev/unity-mcp");
}
if (GUILayout.Button("Client Settings", GUILayout.Height(30)))
{
Windows.MCPForUnityEditorWindow.ShowWindow();
}
EditorGUILayout.EndHorizontal();
}
else
{
DrawErrorStatus("Setup Incomplete - Package Non-Functional");
EditorGUILayout.HelpBox(
"🚨 MCP for Unity CANNOT work - dependencies still missing!\n\n" +
"Install ALL required dependencies before the package will function.",
MessageType.Error
);
var missingDeps = _dependencyResult.GetMissingRequired();
if (missingDeps.Count > 0)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Still Missing:", EditorStyles.boldLabel);
foreach (var dep in missingDeps)
{
EditorGUILayout.LabelField($"✗ {dep.Name}", EditorStyles.label);
}
}
EditorGUILayout.Space();
if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30)))
{
_currentStep = 0;
}
}
}
// Helper methods for consistent UI components
private void DrawSectionTitle(string title, int fontSize = 16)
{
var titleStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = fontSize,
fontStyle = FontStyle.Bold
};
EditorGUILayout.LabelField(title, titleStyle);
EditorGUILayout.Space();
}
private void DrawSuccessStatus(string message)
{
var originalColor = GUI.color;
GUI.color = Color.green;
EditorGUILayout.LabelField($"✓ {message}", EditorStyles.boldLabel);
GUI.color = originalColor;
EditorGUILayout.Space();
}
private void DrawErrorStatus(string message)
{
var originalColor = GUI.color;
GUI.color = Color.red;
EditorGUILayout.LabelField($"✗ {message}", EditorStyles.boldLabel);
GUI.color = originalColor;
EditorGUILayout.Space();
}
private void DrawSimpleDependencyStatus(DependencyStatus dep)
{
EditorGUILayout.BeginHorizontal();
var statusIcon = dep.IsAvailable ? "✓" : "✗";
var statusColor = dep.IsAvailable ? Color.green : Color.red;
var originalColor = GUI.color;
GUI.color = statusColor;
GUILayout.Label(statusIcon, GUILayout.Width(20));
EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel);
GUI.color = originalColor;
if (!dep.IsAvailable && !string.IsNullOrEmpty(dep.ErrorMessage))
{
EditorGUILayout.LabelField($"({dep.ErrorMessage})", EditorStyles.miniLabel);
}
EditorGUILayout.EndHorizontal();
}
private void DrawConfigureStep()
{
DrawSectionTitle("AI Client Configuration");
// Check dependencies first (with caching to avoid heavy operations on every repaint)
if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2)
{
_dependencyResult = DependencyManager.CheckAllDependencies();
}
if (!_dependencyResult.IsSystemReady)
{
DrawErrorStatus("Cannot Configure - System Requirements Not Met");
EditorGUILayout.HelpBox(
"Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.",
MessageType.Warning
);
if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30)))
{
_currentStep = 0;
}
return;
}
EditorGUILayout.LabelField(
"Configure your AI assistants to work with Unity. Select a client below to set it up:",
EditorStyles.wordWrappedLabel
);
EditorGUILayout.Space();
// Client selection and configuration
if (_mcpClients.clients.Count > 0)
{
// Client selector dropdown
string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray();
EditorGUI.BeginChangeCheck();
_selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames);
if (EditorGUI.EndChangeCheck())
{
_selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1);
// Refresh client status when selection changes
CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]);
}
EditorGUILayout.Space();
var selectedClient = _mcpClients.clients[_selectedClientIndex];
DrawClientConfigurationInWizard(selectedClient);
EditorGUILayout.Space();
// Batch configuration option
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel);
EditorGUILayout.LabelField(
"Automatically configure all detected AI clients at once:",
EditorStyles.wordWrappedLabel
);
EditorGUILayout.Space();
if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30)))
{
ConfigureAllClientsInWizard();
}
EditorGUILayout.EndVertical();
}
else
{
EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info);
}
EditorGUILayout.Space();
EditorGUILayout.HelpBox(
"💡 You might need to restart your AI client after configuring.",
MessageType.Info
);
}
private void DrawFooter()
{
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
// Back button
GUI.enabled = _currentStep > 0;
if (GUILayout.Button("Back", GUILayout.Width(60)))
{
_currentStep--;
}
GUILayout.FlexibleSpace();
// Skip button
if (GUILayout.Button("Skip", GUILayout.Width(60)))
{
bool dismiss = EditorUtility.DisplayDialog(
"Skip Setup",
"⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" +
"You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)",
"Skip Anyway",
"Cancel"
);
if (dismiss)
{
SetupWizard.MarkSetupDismissed();
Close();
}
}
// Next/Done button
GUI.enabled = true;
string buttonText = _currentStep == _stepTitles.Length - 1 ? "Done" : "Next";
if (GUILayout.Button(buttonText, GUILayout.Width(80)))
{
if (_currentStep == _stepTitles.Length - 1)
{
SetupWizard.MarkSetupCompleted();
Close();
}
else
{
_currentStep++;
}
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
}
private void DrawClientConfigurationInWizard(McpClient client)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel);
EditorGUILayout.Space();
// Show current status
var statusColor = GetClientStatusColor(client);
var originalColor = GUI.color;
GUI.color = statusColor;
EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label);
GUI.color = originalColor;
EditorGUILayout.Space();
// Configuration buttons
EditorGUILayout.BeginHorizontal();
if (client.mcpType == McpTypes.ClaudeCode)
{
// Special handling for Claude Code
bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude());
if (claudeAvailable)
{
bool isConfigured = client.status == McpStatus.Configured;
string buttonText = isConfigured ? "Unregister" : "Register";
if (GUILayout.Button($"{buttonText} with Claude Code"))
{
if (isConfigured)
{
UnregisterFromClaudeCode(client);
}
else
{
RegisterWithClaudeCode(client);
}
}
}
else
{
EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning);
if (GUILayout.Button("Open Claude Code Website"))
{
Application.OpenURL("https://claude.ai/download");
}
}
}
else
{
// Standard client configuration
if (GUILayout.Button($"Configure {client.name}"))
{
ConfigureClientInWizard(client);
}
if (GUILayout.Button("Manual Setup"))
{
ShowManualSetupInWizard(client);
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
private Color GetClientStatusColor(McpClient client)
{
return client.status switch
{
McpStatus.Configured => Color.green,
McpStatus.Running => Color.green,
McpStatus.Connected => Color.green,
McpStatus.IncorrectPath => Color.yellow,
McpStatus.CommunicationError => Color.yellow,
McpStatus.NoResponse => Color.yellow,
_ => Color.red
};
}
private void ConfigureClientInWizard(McpClient client)
{
try
{
string result = PerformClientConfiguration(client);
EditorUtility.DisplayDialog(
$"{client.name} Configuration",
result,
"OK"
);
// Refresh client status
CheckClientConfiguration(client);
Repaint();
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog(
"Configuration Error",
$"Failed to configure {client.name}: {ex.Message}",
"OK"
);
}
}
private void ConfigureAllClientsInWizard()
{
int successCount = 0;
int totalCount = _mcpClients.clients.Count;
foreach (var client in _mcpClients.clients)
{
try
{
if (client.mcpType == McpTypes.ClaudeCode)
{
if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured)
{
RegisterWithClaudeCode(client);
successCount++;
}
else if (client.status == McpStatus.Configured)
{
successCount++; // Already configured
}
}
else
{
string result = PerformClientConfiguration(client);
if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase))
{
successCount++;
}
}
CheckClientConfiguration(client);
}
catch (System.Exception ex)
{
McpLog.Error($"Failed to configure {client.name}: {ex.Message}");
}
}
EditorUtility.DisplayDialog(
"Batch Configuration Complete",
$"Successfully configured {successCount} out of {totalCount} clients.\n\n" +
"Restart your AI clients for changes to take effect.",
"OK"
);
Repaint();
}
private void RegisterWithClaudeCode(McpClient client)
{
try
{
string pythonDir = McpPathResolver.FindPackagePythonDirectory();
string claudePath = ExecPath.ResolveClaude();
string uvPath = ExecPath.ResolveUv() ?? "uv";
string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py";
if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend()))
{
if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase))
{
CheckClientConfiguration(client);
EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK");
}
else
{
throw new System.Exception($"Registration failed: {stderr}");
}
}
else
{
CheckClientConfiguration(client);
EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK");
}
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK");
}
}
private void UnregisterFromClaudeCode(McpClient client)
{
try
{
string claudePath = ExecPath.ResolveClaude();
if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend()))
{
CheckClientConfiguration(client);
EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK");
}
else
{
throw new System.Exception($"Unregistration failed: {stderr}");
}
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK");
}
}
private string PerformClientConfiguration(McpClient client)
{
// This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
string pythonDir = McpPathResolver.FindPackagePythonDirectory();
if (string.IsNullOrEmpty(pythonDir))
{
return "Manual configuration required - Python server directory not found.";
}
McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
}
private void ShowManualSetupInWizard(McpClient client)
{
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
string pythonDir = McpPathResolver.FindPackagePythonDirectory();
string uvPath = ServerInstaller.FindUvPath();
if (string.IsNullOrEmpty(uvPath))
{
EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK");
return;
}
// Build manual configuration using the sophisticated helper logic
string result = McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
string manualConfig;
if (result == "Configured successfully")
{
// Read back the configuration that was written
try
{
manualConfig = System.IO.File.ReadAllText(configPath);
}
catch
{
manualConfig = "Configuration written successfully, but could not read back for display.";
}
}
else
{
manualConfig = $"Configuration failed: {result}";
}
EditorUtility.DisplayDialog(
$"Manual Setup - {client.name}",
$"Configuration file location:\n{configPath}\n\n" +
$"Configuration result:\n{manualConfig}",
"OK"
);
}
private void CheckClientConfiguration(McpClient client)
{
// Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic
try
{
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
if (System.IO.File.Exists(configPath))
{
client.configStatus = "Configured";
client.status = McpStatus.Configured;
}
else
{
client.configStatus = "Not Configured";
client.status = McpStatus.NotConfigured;
}
}
catch
{
client.configStatus = "Error";
client.status = McpStatus.Error;
}
}
private void OpenInstallationUrls()
{
var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls();
bool openPython = EditorUtility.DisplayDialog(
"Open Installation URLs",
"Open Python installation page?",
"Yes",
"No"
);
if (openPython)
{
Application.OpenURL(pythonUrl);
}
bool openUV = EditorUtility.DisplayDialog(
"Open Installation URLs",
"Open UV installation page?",
"Yes",
"No"
);
if (openUV)
{
Application.OpenURL(uvUrl);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 45678901234abcdef0123456789abcde
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c97b83a6ac92a704b864eef27c3d285b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
namespace MCPForUnity.Editor.Tools
{
/// <summary>
/// Registry for all MCP command handlers via reflection.
/// </summary>
public static class CommandRegistry
{
private static readonly Dictionary<string, Func<JObject, object>> _handlers = new();
private static bool _initialized = false;
/// <summary>
/// Initialize and auto-discover all tools marked with [McpForUnityTool]
/// </summary>
public static void Initialize()
{
if (_initialized) return;
AutoDiscoverTools();
_initialized = true;
}
/// <summary>
/// Convert PascalCase or camelCase to snake_case
/// </summary>
private static string ToSnakeCase(string name)
{
if (string.IsNullOrEmpty(name)) return name;
// Insert underscore before uppercase letters (except first)
var s1 = Regex.Replace(name, "(.)([A-Z][a-z]+)", "$1_$2");
var s2 = Regex.Replace(s1, "([a-z0-9])([A-Z])", "$1_$2");
return s2.ToLower();
}
/// <summary>
/// Auto-discover all types with [McpForUnityTool] attribute
/// </summary>
private static void AutoDiscoverTools()
{
try
{
var toolTypes = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic)
.SelectMany(a =>
{
try { return a.GetTypes(); }
catch { return new Type[0]; }
})
.Where(t => t.GetCustomAttribute<McpForUnityToolAttribute>() != null);
foreach (var type in toolTypes)
{
RegisterToolType(type);
}
McpLog.Info($"Auto-discovered {_handlers.Count} tools");
}
catch (Exception ex)
{
McpLog.Error($"Failed to auto-discover MCP tools: {ex.Message}");
}
}
private static void RegisterToolType(Type type)
{
var attr = type.GetCustomAttribute<McpForUnityToolAttribute>();
// Get command name (explicit or auto-generated)
string commandName = attr.CommandName;
if (string.IsNullOrEmpty(commandName))
{
commandName = ToSnakeCase(type.Name);
}
// Check for duplicate command names
if (_handlers.ContainsKey(commandName))
{
McpLog.Warn(
$"Duplicate command name '{commandName}' detected. " +
$"Tool {type.Name} will override previously registered handler."
);
}
// Find HandleCommand method
var method = type.GetMethod(
"HandleCommand",
BindingFlags.Public | BindingFlags.Static,
null,
new[] { typeof(JObject) },
null
);
if (method == null)
{
McpLog.Warn(
$"MCP tool {type.Name} is marked with [McpForUnityTool] " +
$"but has no public static HandleCommand(JObject) method"
);
return;
}
try
{
var handler = (Func<JObject, object>)Delegate.CreateDelegate(
typeof(Func<JObject, object>),
method
);
_handlers[commandName] = handler;
}
catch (Exception ex)
{
McpLog.Error($"Failed to register tool {type.Name}: {ex.Message}");
}
}
/// <summary>
/// Get a command handler by name
/// </summary>
public static Func<JObject, object> GetHandler(string commandName)
{
if (!_handlers.TryGetValue(commandName, out var handler))
{
throw new InvalidOperationException(
$"Unknown or unsupported command type: {commandName}"
);
}
return handler;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5b61b5a84813b5749a5c64422694a0fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: de90a1d9743a2874cb235cf0b83444b1
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