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
parent
ff736012fa
commit
e9b1ae44c5
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 31e7fac5858840340a75cc6df0ad3d9e
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")]
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: be61633e00d934610ac1ff8192ffbe3d
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e59036660cc33d24596fbbf6d4657a83
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: de8f5721c34f7194392e9d8c7d0226c0
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 711b86bbc1f661e4fb2c822e14970e16
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 221a4d6e595be6897a5b17b77aedd4d0
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f6789012345678901234abcdef012345
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b2c3d4e5f6789012345678901234abcd
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace MCPForUnity.Editor.Dependencies.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Result of a comprehensive dependency check
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class DependencyCheckResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// List of all dependency statuses checked
|
||||||
|
/// </summary>
|
||||||
|
public List<DependencyStatus> Dependencies { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overall system readiness for MCP operations
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSystemReady { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether all required dependencies are available
|
||||||
|
/// </summary>
|
||||||
|
public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether any optional dependencies are missing
|
||||||
|
/// </summary>
|
||||||
|
public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Summary message about the dependency state
|
||||||
|
/// </summary>
|
||||||
|
public string Summary { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recommended next steps for the user
|
||||||
|
/// </summary>
|
||||||
|
public List<string> RecommendedActions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp when this check was performed
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CheckedAt { get; set; }
|
||||||
|
|
||||||
|
public DependencyCheckResult()
|
||||||
|
{
|
||||||
|
Dependencies = new List<DependencyStatus>();
|
||||||
|
RecommendedActions = new List<string>();
|
||||||
|
CheckedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get dependencies by availability status
|
||||||
|
/// </summary>
|
||||||
|
public List<DependencyStatus> GetMissingDependencies()
|
||||||
|
{
|
||||||
|
return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get missing required dependencies
|
||||||
|
/// </summary>
|
||||||
|
public List<DependencyStatus> GetMissingRequired()
|
||||||
|
{
|
||||||
|
return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a user-friendly summary of the dependency state
|
||||||
|
/// </summary>
|
||||||
|
public void GenerateSummary()
|
||||||
|
{
|
||||||
|
var missing = GetMissingDependencies();
|
||||||
|
var missingRequired = GetMissingRequired();
|
||||||
|
|
||||||
|
if (missing.Count == 0)
|
||||||
|
{
|
||||||
|
Summary = "All dependencies are available and ready.";
|
||||||
|
IsSystemReady = true;
|
||||||
|
}
|
||||||
|
else if (missingRequired.Count == 0)
|
||||||
|
{
|
||||||
|
Summary = $"System is ready. {missing.Count} optional dependencies are missing.";
|
||||||
|
IsSystemReady = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing.";
|
||||||
|
IsSystemReady = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 789012345678901234abcdef01234567
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MCPForUnity.Editor.Dependencies.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the status of a dependency check
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class DependencyStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the dependency being checked
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the dependency is available and functional
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAvailable { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Version information if available
|
||||||
|
/// </summary>
|
||||||
|
public string Version { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path to the dependency executable/installation
|
||||||
|
/// </summary>
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional details about the dependency status
|
||||||
|
/// </summary>
|
||||||
|
public string Details { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error message if dependency check failed
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this dependency is required for basic functionality
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRequired { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Suggested installation method or URL
|
||||||
|
/// </summary>
|
||||||
|
public string InstallationHint { get; set; }
|
||||||
|
|
||||||
|
public DependencyStatus(string name, bool isRequired = true)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
IsRequired = isRequired;
|
||||||
|
IsAvailable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var status = IsAvailable ? "✓" : "✗";
|
||||||
|
var version = !string.IsNullOrEmpty(Version) ? $" ({Version})" : "";
|
||||||
|
return $"{status} {Name}{version}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6789012345678901234abcdef0123456
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c3d4e5f6789012345678901234abcdef
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9012345678901234abcdef0123456789
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2345678901234abcdef0123456789abc
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 12345678901234abcdef0123456789ab
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 44d715aedea2b8b41bf914433bbb2c49
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 012345678901234abcdef0123456789a
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c11944bcfb9ec4576bab52874b7df584
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ea652131dcdaa44ca8cb35cd1191be3f
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 94cb070dc5e15024da86150b27699ca0
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1d42f5b5ea5d4d43ad1a771e14bda2a0
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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("\"", "\\\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b3e68082ffc0b4cd39d3747673a4cc22
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5c07c3369f73943919d9e086a81d1dcc
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8f2b7b3e9c3e4a0f9b2a1d4c7e6f5a12
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 64b8ff807bc9a401c82015cbafccffac
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f69ad468942b74c0ea24e3e8e5f21a4b
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e45ac2a13b4c1ba468b8e3aa67b292ca
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9e2c3f8a4f4f48d8a4c1b7b8e3f5a1c2
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2c76f0c7ff138ba4a952481e04bc3974
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b82eaef548d164ca095f17db64d15af8
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 19e6eaa637484e9fa19f9a0459809de2
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a1b2c3d4e5f6789012345678901234ab
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 80c09a76b944f8c4691e06c4d76c4be8
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5862c6a6d0a914f4d83224f8d039cf7b
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b8f3c2d1e7a94f6c8a9b5e3d2c1a0f9e
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f8514fd42f23cb641a36e52550825b35
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 98f702da6ca044be59a864a9419c4eab
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 96dc847eb7f7a45e0b91241db934a4be
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 16d3ab36890b6c14f9afeabee30e03e3
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6754c84e5deb74749bc3a19e0c9aa280
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5fae9d995f514e9498e9613e2cdbeca9
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace MCPForUnity.Editor.Models
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class McpConfigServers
|
||||||
|
{
|
||||||
|
[JsonProperty("unityMCP")]
|
||||||
|
public McpConfigServer unityMCP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bcb583553e8173b49be71a5c43bd9502
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b1afa56984aec0d41808edcebf805e6a
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace MCPForUnity.Editor.Models
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class McpConfig
|
||||||
|
{
|
||||||
|
[JsonProperty("mcpServers")]
|
||||||
|
public McpConfigServers mcpServers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c17c09908f0c1524daa8b6957ce1f7f5
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: aa63057c9e5282d4887352578bf49971
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace MCPForUnity.Editor.Models
|
||||||
|
{
|
||||||
|
public enum McpTypes
|
||||||
|
{
|
||||||
|
ClaudeCode,
|
||||||
|
ClaudeDesktop,
|
||||||
|
Codex,
|
||||||
|
Cursor,
|
||||||
|
Kiro,
|
||||||
|
VSCode,
|
||||||
|
Windsurf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e4e45386fcc282249907c2e3c7e5d9c6
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 600c9cb20c329d761bfa799158a87bac
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 345678901234abcdef0123456789abcd
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 45678901234abcdef0123456789abcde
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c97b83a6ac92a704b864eef27c3d285b
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue