97ac0f71f5
https://assetstore.unity.com/packages/tools/generative-ai/synaptic-ai-pro-natural-language-control-for-unity-336030
1348 lines
47 KiB
C#
1348 lines
47 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using UnityEngine;
|
||
#if UNITY_EDITOR
|
||
using UnityEditor;
|
||
#endif
|
||
using Debug = UnityEngine.Debug;
|
||
|
||
namespace SynapticPro
|
||
{
|
||
/// <summary>
|
||
/// One-touch MCP server setup manager
|
||
/// Handles automatic installation of Git, Node.js, npm and various AI configurations
|
||
/// </summary>
|
||
public class NexusMCPSetupManager
|
||
{
|
||
private static NexusMCPSetupManager instance;
|
||
private static int mcpServerProcessId = -1;
|
||
public static NexusMCPSetupManager Instance
|
||
{
|
||
get
|
||
{
|
||
if (instance == null)
|
||
instance = new NexusMCPSetupManager();
|
||
return instance;
|
||
}
|
||
}
|
||
|
||
private string projectPath;
|
||
private string mcpServerPath;
|
||
private string toolsPath;
|
||
|
||
public class SetupStatus
|
||
{
|
||
public bool isGitInstalled;
|
||
public bool isNodeInstalled;
|
||
public bool isNpmInstalled;
|
||
public bool isMCPInstalled;
|
||
public bool isConfigured;
|
||
public string gitVersion;
|
||
public string nodeVersion;
|
||
public string npmVersion;
|
||
public List<string> installedTools = new List<string>();
|
||
public Dictionary<string, bool> aiConfigurations = new Dictionary<string, bool>();
|
||
}
|
||
|
||
private SetupStatus currentStatus = new SetupStatus();
|
||
|
||
private NexusMCPSetupManager()
|
||
{
|
||
projectPath = Application.dataPath.Replace("/Assets", "");
|
||
mcpServerPath = Path.Combine(projectPath, "MCPServer");
|
||
toolsPath = Path.Combine(projectPath, "Tools");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Check setup status
|
||
/// </summary>
|
||
public async Task<SetupStatus> CheckSetupStatus()
|
||
{
|
||
currentStatus = new SetupStatus();
|
||
|
||
// Check Git
|
||
currentStatus.gitVersion = await CheckCommand("git", "--version");
|
||
currentStatus.isGitInstalled = !string.IsNullOrEmpty(currentStatus.gitVersion);
|
||
|
||
// Check Node.js
|
||
currentStatus.nodeVersion = await CheckCommand("node", "--version");
|
||
currentStatus.isNodeInstalled = !string.IsNullOrEmpty(currentStatus.nodeVersion);
|
||
|
||
// Check npm
|
||
currentStatus.npmVersion = await CheckCommand("npm", "--version");
|
||
currentStatus.isNpmInstalled = !string.IsNullOrEmpty(currentStatus.npmVersion);
|
||
|
||
// Check MCP server directory and version
|
||
if (Directory.Exists(mcpServerPath) && File.Exists(Path.Combine(mcpServerPath, "package.json")))
|
||
{
|
||
try
|
||
{
|
||
// Check package.json version
|
||
var packageJsonPath = Path.Combine(mcpServerPath, "package.json");
|
||
var packageJsonContent = File.ReadAllText(packageJsonPath);
|
||
|
||
// Simple version check (avoid JSON parsing dependency)
|
||
if (packageJsonContent.Contains("\"version\": \"1.2.0\"") ||
|
||
packageJsonContent.Contains("\"version\":\"1.2.0\""))
|
||
{
|
||
currentStatus.isMCPInstalled = true;
|
||
Debug.Log("[Synaptic] MCPServer v1.2.0 detected - up to date");
|
||
}
|
||
else
|
||
{
|
||
// Old version detected - force regeneration
|
||
currentStatus.isMCPInstalled = false;
|
||
Debug.LogWarning("[Synaptic] MCPServer outdated version detected - will regenerate");
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning($"[Synaptic] Failed to check MCPServer version: {e.Message}");
|
||
currentStatus.isMCPInstalled = false;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
currentStatus.isMCPInstalled = false;
|
||
}
|
||
|
||
// Check AI configurations
|
||
CheckAIConfigurations();
|
||
|
||
return currentStatus;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Execute one-touch setup
|
||
/// </summary>
|
||
public async Task<bool> RunCompleteSetup(Action<string> progressCallback = null)
|
||
{
|
||
try
|
||
{
|
||
progressCallback?.Invoke("Starting setup...");
|
||
|
||
// 1. Install necessary tools
|
||
if (!currentStatus.isGitInstalled)
|
||
{
|
||
progressCallback?.Invoke("Installing Git...");
|
||
await InstallGit();
|
||
}
|
||
|
||
if (!currentStatus.isNodeInstalled || !currentStatus.isNpmInstalled)
|
||
{
|
||
progressCallback?.Invoke("Installing Node.js and npm...");
|
||
await InstallNodeJS();
|
||
}
|
||
|
||
// 2. Setup MCP server
|
||
progressCallback?.Invoke("Building MCP Server...");
|
||
await SetupMCPServer();
|
||
|
||
// 3. Install dependencies
|
||
progressCallback?.Invoke("Installing dependencies...");
|
||
await InstallDependencies();
|
||
|
||
// 4. Install Unity integration tools
|
||
progressCallback?.Invoke("Setting up Unity integration tools...");
|
||
await SetupUnityTools();
|
||
|
||
// 5. AI configuration
|
||
progressCallback?.Invoke("Configuring AI settings...");
|
||
await ConfigureAIServices();
|
||
|
||
// 6. Generate configuration files
|
||
progressCallback?.Invoke("Generating configuration files...");
|
||
await GenerateConfigFiles();
|
||
|
||
progressCallback?.Invoke("Setup complete!");
|
||
|
||
// Check final state
|
||
await CheckSetupStatus();
|
||
|
||
return true;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[MCP Setup] Setup error: {e.Message}");
|
||
progressCallback?.Invoke($"Error: {e.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Auto-install Git
|
||
/// </summary>
|
||
private Task InstallGit()
|
||
{
|
||
return Task.Run(async () =>
|
||
{
|
||
var platform = Application.platform;
|
||
|
||
if (platform == RuntimePlatform.OSXEditor)
|
||
{
|
||
// macOS - Install via Homebrew
|
||
var hasHomebrew = await CheckCommand("brew", "--version");
|
||
if (string.IsNullOrEmpty(hasHomebrew))
|
||
{
|
||
// Let user install Homebrew
|
||
#if UNITY_EDITOR
|
||
EditorUtility.DisplayDialog(
|
||
"Homebrew Required",
|
||
"Homebrew is not installed.\nPlease install from https://brew.sh",
|
||
"OK"
|
||
);
|
||
#endif
|
||
Application.OpenURL("https://brew.sh");
|
||
throw new Exception("Homebrew is not installed");
|
||
}
|
||
|
||
await RunCommand("brew", "install git");
|
||
}
|
||
else if (platform == RuntimePlatform.WindowsEditor)
|
||
{
|
||
// Windows - Download Git for Windows
|
||
var gitInstallerPath = Path.Combine(toolsPath, "GitInstaller.exe");
|
||
|
||
if (!Directory.Exists(toolsPath))
|
||
Directory.CreateDirectory(toolsPath);
|
||
|
||
// Download Git for Windows
|
||
// Note: Git download page is HTML, so use direct download URL
|
||
using (var client = new System.Net.WebClient())
|
||
{
|
||
// Use latest 2.51.0 version
|
||
var gitDownloadUrl = "https://github.com/git-for-windows/git/releases/download/v2.51.0.windows.1/Git-2.51.0-64-bit.exe";
|
||
await client.DownloadFileTaskAsync(gitDownloadUrl, gitInstallerPath);
|
||
}
|
||
|
||
// Silent install
|
||
await RunCommand(gitInstallerPath, "/VERYSILENT /NORESTART");
|
||
}
|
||
|
||
// Update path
|
||
RefreshEnvironmentPath();
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Auto-install Node.js
|
||
/// </summary>
|
||
private Task InstallNodeJS()
|
||
{
|
||
return Task.Run(async () =>
|
||
{
|
||
var platform = Application.platform;
|
||
|
||
if (platform == RuntimePlatform.OSXEditor)
|
||
{
|
||
// macOS - via Homebrew
|
||
await RunCommand("brew", "install node");
|
||
}
|
||
else if (platform == RuntimePlatform.WindowsEditor)
|
||
{
|
||
// Windows - Download Node.js installer
|
||
var nodeInstallerPath = Path.Combine(toolsPath, "NodeInstaller.msi");
|
||
|
||
if (!Directory.Exists(toolsPath))
|
||
Directory.CreateDirectory(toolsPath);
|
||
|
||
// Download Node.js LTS v22.11.0
|
||
using (var client = new System.Net.WebClient())
|
||
{
|
||
await client.DownloadFileTaskAsync(
|
||
"https://nodejs.org/dist/v22.11.0/node-v22.11.0-x64.msi",
|
||
nodeInstallerPath
|
||
);
|
||
}
|
||
|
||
// Silent install
|
||
await RunCommand("msiexec", $"/i \"{nodeInstallerPath}\" /qn");
|
||
}
|
||
|
||
RefreshEnvironmentPath();
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Setup MCP server
|
||
/// </summary>
|
||
private Task SetupMCPServer()
|
||
{
|
||
return Task.Run(() =>
|
||
{
|
||
// Create MCP server directory
|
||
if (!Directory.Exists(mcpServerPath))
|
||
{
|
||
Directory.CreateDirectory(mcpServerPath);
|
||
}
|
||
|
||
// Generate package.json (ES module support)
|
||
var packageJson = @"{
|
||
""name"": ""unity-mcp-server"",
|
||
""version"": ""1.2.0"",
|
||
""description"": ""MCP Server for Unity Integration"",
|
||
""main"": ""index.js"",
|
||
""type"": ""module"",
|
||
""scripts"": {
|
||
""start"": ""node index.js"",
|
||
""dev"": ""nodemon index.js""
|
||
},
|
||
""dependencies"": {
|
||
""@modelcontextprotocol/sdk"": ""^1.18.1"",
|
||
""express"": ""^4.18.2"",
|
||
""ws"": ""^8.13.0"",
|
||
""cors"": ""^2.8.5"",
|
||
""dotenv"": ""^16.0.3"",
|
||
""zod"": ""^3.23.8""
|
||
},
|
||
""devDependencies"": {
|
||
""nodemon"": ""^3.0.1""
|
||
}
|
||
}";
|
||
|
||
File.WriteAllText(Path.Combine(mcpServerPath, "package.json"), packageJson);
|
||
|
||
// Generate MCP server main file (ES module support)
|
||
var serverCode = @"import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||
import express from 'express';
|
||
import cors from 'cors';
|
||
import { WebSocketServer } from 'ws';
|
||
import dotenv from 'dotenv';
|
||
|
||
dotenv.config();
|
||
|
||
const app = express();
|
||
app.use(cors());
|
||
app.use(express.json());
|
||
|
||
// Initialize MCP server
|
||
const server = new Server(
|
||
{
|
||
name: 'unity-mcp-server',
|
||
version: '1.2.0',
|
||
},
|
||
{
|
||
capabilities: {
|
||
tools: {},
|
||
},
|
||
}
|
||
);
|
||
|
||
// Unity operation tools
|
||
server.setRequestHandler('tools/list', async () => {
|
||
return {
|
||
tools: [
|
||
{
|
||
name: 'unity_create',
|
||
description: 'Create Unity GameObjects and components',
|
||
inputSchema: {
|
||
type: 'object',
|
||
properties: {
|
||
objectType: { type: 'string' },
|
||
name: { type: 'string' },
|
||
position: {
|
||
type: 'object',
|
||
properties: {
|
||
x: { type: 'number' },
|
||
y: { type: 'number' },
|
||
z: { type: 'number' }
|
||
}
|
||
}
|
||
},
|
||
},
|
||
},
|
||
],
|
||
};
|
||
});
|
||
|
||
server.setRequestHandler('tools/call', async (request) => {
|
||
if (request.params.name === 'unity_create') {
|
||
return await sendUnityCommand('create', request.params.arguments);
|
||
}
|
||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||
});
|
||
|
||
// WebSocket server
|
||
const wss = new WebSocketServer({ port: 8090 });
|
||
|
||
wss.on('connection', (ws) => {
|
||
console.log('Unity client connected');
|
||
|
||
ws.on('message', (message) => {
|
||
console.log('Received from Unity:', message.toString());
|
||
});
|
||
});
|
||
|
||
async function sendUnityCommand(command, params) {
|
||
// Send to Unity client
|
||
const message = JSON.stringify({ command, params });
|
||
wss.clients.forEach(client => {
|
||
if (client.readyState === WebSocket.OPEN) {
|
||
client.send(message);
|
||
}
|
||
});
|
||
return { success: true };
|
||
}
|
||
|
||
// HTTP endpoints
|
||
app.post('/api/chat', async (req, res) => {
|
||
try {
|
||
const { message } = req.body;
|
||
// Simple echo response
|
||
res.json({ response: `Unity MCP Server received: ${message}` });
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
const PORT = process.env.PORT || 3000;
|
||
app.listen(PORT, () => {
|
||
console.log(`Unity MCP Server running on port ${PORT}`);
|
||
console.log(`WebSocket server running on port 8090`);
|
||
});
|
||
|
||
// Start MCP server via Stdio
|
||
async function runMCPServer() {
|
||
const transport = new StdioServerTransport();
|
||
await server.connect(transport);
|
||
console.log('MCP Server connected via stdio');
|
||
}
|
||
|
||
// Start both servers
|
||
runMCPServer().catch(console.error);
|
||
";
|
||
|
||
|
||
File.WriteAllText(Path.Combine(mcpServerPath, "index.js"), serverCode);
|
||
|
||
// Generate .env file
|
||
var envContent = @"PORT=3000
|
||
CLAUDE_API_KEY=
|
||
GEMINI_API_KEY=
|
||
OPENAI_API_KEY=
|
||
";
|
||
|
||
File.WriteAllText(Path.Combine(mcpServerPath, ".env"), envContent);
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Install dependencies
|
||
/// </summary>
|
||
private async Task InstallDependencies()
|
||
{
|
||
// Run npm install in MCP server directory
|
||
await RunCommand("npm", "install", mcpServerPath);
|
||
|
||
// Install MCP-related tools
|
||
// Claude CLI doesn't officially exist as npm package, so commented out
|
||
// await RunCommand("npm", "install -g claude-cli");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Setup Unity integration tools
|
||
/// </summary>
|
||
private async Task SetupUnityTools()
|
||
{
|
||
// Create Unity WebSocket client
|
||
var clientCode = @"using System;
|
||
using System.Collections.Generic;
|
||
using System.Net.WebSockets;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using UnityEngine;
|
||
using Newtonsoft.Json;
|
||
|
||
namespace SynapticPro.MCP
|
||
{
|
||
public class MCPUnityClient : MonoBehaviour
|
||
{
|
||
private ClientWebSocket ws;
|
||
private CancellationTokenSource cancellationToken;
|
||
private string serverUrl = ""ws://localhost:8090"";
|
||
private Queue<MCPCommand> commandQueue = new Queue<MCPCommand>();
|
||
private bool isConnected = false;
|
||
|
||
[Serializable]
|
||
public class MCPCommand
|
||
{
|
||
public string command;
|
||
public Dictionary<string, object> parameters;
|
||
}
|
||
|
||
void Start()
|
||
{
|
||
ConnectToMCPServer();
|
||
}
|
||
|
||
async void ConnectToMCPServer()
|
||
{
|
||
try
|
||
{
|
||
ws = new ClientWebSocket();
|
||
cancellationToken = new CancellationTokenSource();
|
||
|
||
await ws.ConnectAsync(new Uri(serverUrl), cancellationToken.Token);
|
||
isConnected = true;
|
||
Debug.Log(""[MCP Client] Connected to MCP Server"");
|
||
|
||
// Start message receiving loop
|
||
_ = ReceiveLoop();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($""[MCP Client] Connection error: {e.Message}"");
|
||
}
|
||
}
|
||
|
||
async Task ReceiveLoop()
|
||
{
|
||
var buffer = new ArraySegment<byte>(new byte[4096]);
|
||
|
||
while (isConnected && ws.State == WebSocketState.Open)
|
||
{
|
||
try
|
||
{
|
||
var result = await ws.ReceiveAsync(buffer, cancellationToken.Token);
|
||
|
||
if (result.MessageType == WebSocketMessageType.Text)
|
||
{
|
||
var message = Encoding.UTF8.GetString(buffer.Array, 0, result.Count);
|
||
var command = JsonConvert.DeserializeObject<MCPCommand>(message);
|
||
commandQueue.Enqueue(command);
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($""[MCP Client] Receive error: {e.Message}"");
|
||
isConnected = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
while (commandQueue.Count > 0)
|
||
{
|
||
var command = commandQueue.Dequeue();
|
||
ExecuteCommand(command);
|
||
}
|
||
}
|
||
|
||
void ExecuteCommand(MCPCommand command)
|
||
{
|
||
switch (command.command)
|
||
{
|
||
case ""create"":
|
||
CreateGameObject(command.parameters);
|
||
break;
|
||
// Add other commands
|
||
}
|
||
}
|
||
|
||
void CreateGameObject(Dictionary<string, object> parameters)
|
||
{
|
||
var name = parameters.ContainsKey(""name"") ? parameters[""name""].ToString() : ""GameObject"";
|
||
var go = new GameObject(name);
|
||
|
||
if (parameters.ContainsKey(""position""))
|
||
{
|
||
// Set position
|
||
}
|
||
|
||
Debug.Log($""[MCP Client] Created GameObject: {name}"");
|
||
}
|
||
|
||
async void OnDestroy()
|
||
{
|
||
if (ws != null && ws.State == WebSocketState.Open)
|
||
{
|
||
cancellationToken.Cancel();
|
||
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, ""Client closing"", CancellationToken.None);
|
||
ws.Dispose();
|
||
}
|
||
}
|
||
}
|
||
}";
|
||
|
||
var clientPath = Path.Combine(Application.dataPath, "Synaptic", "Scripts", "MCP", "MCPUnityClient.cs");
|
||
var mcpDir = Path.GetDirectoryName(clientPath);
|
||
|
||
if (!Directory.Exists(mcpDir))
|
||
{
|
||
Directory.CreateDirectory(mcpDir);
|
||
}
|
||
|
||
File.WriteAllText(clientPath, clientCode);
|
||
|
||
// Install required packages
|
||
await InstallUnityPackage("com.unity.nuget.newtonsoft-json", "3.2.1");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Auto-configure AI services
|
||
/// </summary>
|
||
private async Task ConfigureAIServices()
|
||
{
|
||
var configPath = Path.Combine(mcpServerPath, "ai-config");
|
||
if (!Directory.Exists(configPath))
|
||
{
|
||
Directory.CreateDirectory(configPath);
|
||
}
|
||
|
||
// Claude configuration
|
||
var claudeConfig = @"{
|
||
""provider"": ""anthropic"",
|
||
""model"": ""claude-3-opus-20240229"",
|
||
""temperature"": 0.7,
|
||
""max_tokens"": 4096,
|
||
""tools"": [""unity_create"", ""unity_modify"", ""unity_delete""]
|
||
}";
|
||
File.WriteAllText(Path.Combine(configPath, "claude.json"), claudeConfig);
|
||
|
||
// Gemini configuration
|
||
var geminiConfig = @"{
|
||
""provider"": ""google"",
|
||
""model"": ""gemini-pro"",
|
||
""temperature"": 0.8,
|
||
""tools"": [""unity_create"", ""unity_modify""]
|
||
}";
|
||
File.WriteAllText(Path.Combine(configPath, "gemini.json"), geminiConfig);
|
||
|
||
// Copilot configuration
|
||
var copilotConfig = @"{
|
||
""provider"": ""github"",
|
||
""model"": ""gpt-4"",
|
||
""temperature"": 0.5,
|
||
""tools"": [""unity_create"", ""unity_modify"", ""code_generation""]
|
||
}";
|
||
File.WriteAllText(Path.Combine(configPath, "copilot.json"), copilotConfig);
|
||
|
||
currentStatus.aiConfigurations["Claude"] = true;
|
||
currentStatus.aiConfigurations["Gemini"] = true;
|
||
currentStatus.aiConfigurations["Copilot"] = true;
|
||
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Generate configuration files
|
||
/// </summary>
|
||
private async Task GenerateConfigFiles()
|
||
{
|
||
// MCP configuration file
|
||
var mcpConfig = @"{
|
||
""servers"": {
|
||
""unity"": {
|
||
""command"": ""node"",
|
||
""args"": [""index.js""],
|
||
""cwd"": """ + mcpServerPath.Replace("\\", "/") + @""",
|
||
""env"": {
|
||
""NODE_ENV"": ""production""
|
||
}
|
||
}
|
||
},
|
||
""tools"": {
|
||
""unity_create"": {
|
||
""description"": ""Create Unity GameObjects""
|
||
},
|
||
""unity_modify"": {
|
||
""description"": ""Modify Unity GameObjects""
|
||
},
|
||
""unity_delete"": {
|
||
""description"": ""Delete Unity GameObjects""
|
||
}
|
||
}
|
||
}";
|
||
|
||
// Save MCP configuration file in project
|
||
var mcpConfigPath = Path.Combine(mcpServerPath, "mcp-config.json");
|
||
File.WriteAllText(mcpConfigPath, mcpConfig);
|
||
|
||
// Generate README with documentation links for reference
|
||
var readmeContent = @"# Unity MCP Server
|
||
|
||
## Documentation
|
||
- MCP Documentation: https://modelcontextprotocol.io/docs
|
||
- MCP Specification: https://modelcontextprotocol.io/specification/2025-06-18
|
||
- MCP SDK: https://www.npmjs.com/package/@modelcontextprotocol/sdk
|
||
|
||
## API Keys
|
||
- Claude API: https://console.anthropic.com/
|
||
- Gemini API: https://aistudio.google.com/app/apikey
|
||
- OpenAI API: https://platform.openai.com/api-keys
|
||
|
||
## Setup
|
||
1. Run `npm install` to install dependencies
|
||
2. Configure API keys in .env file
|
||
3. Run `npm start` to start the server
|
||
";
|
||
File.WriteAllText(Path.Combine(mcpServerPath, "README.md"), readmeContent);
|
||
|
||
// Startup script
|
||
var startScript = @"#!/bin/bash
|
||
cd """ + mcpServerPath + @"""
|
||
npm start
|
||
";
|
||
|
||
var startScriptPath = Path.Combine(mcpServerPath, "start.sh");
|
||
File.WriteAllText(startScriptPath, startScript);
|
||
|
||
if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.LinuxEditor)
|
||
{
|
||
await RunCommand("chmod", $"+x \"{startScriptPath}\"");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Command execution helper
|
||
/// </summary>
|
||
private async Task<string> CheckCommand(string command, string args)
|
||
{
|
||
try
|
||
{
|
||
var resolvedCommand = ResolveCommandPath(command);
|
||
if (string.IsNullOrEmpty(resolvedCommand)) return ""; // Not found, skip
|
||
|
||
var process = new Process
|
||
{
|
||
StartInfo = new ProcessStartInfo
|
||
{
|
||
FileName = resolvedCommand,
|
||
Arguments = args,
|
||
UseShellExecute = false,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
CreateNoWindow = true
|
||
}
|
||
};
|
||
|
||
process.Start();
|
||
var output = await process.StandardOutput.ReadToEndAsync();
|
||
process.WaitForExit(5000); // 5秒タイムアウト
|
||
if (!process.HasExited) process.Kill();
|
||
|
||
return output.Trim();
|
||
}
|
||
catch
|
||
{
|
||
return "";
|
||
}
|
||
}
|
||
|
||
private string ResolveCommandPath(string command)
|
||
{
|
||
bool isWindows = Application.platform == RuntimePlatform.WindowsEditor;
|
||
string executableName = isWindows ? (command.EndsWith(".exe") ? command : command + ".exe") : command;
|
||
|
||
// First search with which/where command (most reliable)
|
||
try
|
||
{
|
||
var whichResult = isWindows ? RunWhereCommand(command) : RunWhichCommand(command);
|
||
if (!string.IsNullOrEmpty(whichResult) && File.Exists(whichResult))
|
||
{
|
||
Debug.Log($"[MCP Setup] Detected {command}: {whichResult}");
|
||
return whichResult;
|
||
}
|
||
}
|
||
catch { }
|
||
|
||
// Search common command paths (OS-specific)
|
||
var commonPaths = new System.Collections.Generic.List<string>();
|
||
|
||
if (isWindows)
|
||
{
|
||
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||
|
||
commonPaths.AddRange(new[]
|
||
{
|
||
// Node.js標準インストール
|
||
Path.Combine(programFiles, "nodejs"),
|
||
Path.Combine(programFilesX86, "nodejs"),
|
||
Path.Combine(localAppData, "Programs", "nodejs"),
|
||
|
||
// Git
|
||
Path.Combine(programFiles, "Git", "cmd"),
|
||
Path.Combine(programFiles, "Git", "bin"),
|
||
Path.Combine(localAppData, "Programs", "Git", "cmd"),
|
||
|
||
// nvm-windows
|
||
Path.Combine(appData, "nvm"),
|
||
Path.Combine(userProfile, ".nvm"),
|
||
|
||
// volta
|
||
Path.Combine(localAppData, "Volta", "bin"),
|
||
Path.Combine(userProfile, ".volta", "bin"),
|
||
|
||
// fnm
|
||
Path.Combine(localAppData, "fnm"),
|
||
Path.Combine(userProfile, ".fnm"),
|
||
|
||
// npm global
|
||
Path.Combine(appData, "npm"),
|
||
|
||
// D:ドライブ(セカンダリドライブにインストールするユーザー対策)
|
||
"D:\\nodejs",
|
||
"D:\\Program Files\\nodejs",
|
||
"D:\\Program Files (x86)\\nodejs",
|
||
"D:\\Program Files\\Git\\cmd",
|
||
"D:\\nvm",
|
||
"D:\\nvm\\nodejs",
|
||
|
||
// E:ドライブ
|
||
"E:\\nodejs",
|
||
"E:\\Program Files\\nodejs",
|
||
|
||
// その他のドライブ(F-I)
|
||
"F:\\nodejs", "F:\\Program Files\\nodejs",
|
||
"G:\\nodejs", "G:\\Program Files\\nodejs",
|
||
"H:\\nodejs", "H:\\Program Files\\nodejs",
|
||
"I:\\nodejs", "I:\\Program Files\\nodejs",
|
||
});
|
||
}
|
||
else
|
||
{
|
||
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||
commonPaths.AddRange(new[]
|
||
{
|
||
"/opt/homebrew/bin",
|
||
"/usr/local/bin",
|
||
"/usr/bin",
|
||
"/bin",
|
||
"/usr/local/Cellar",
|
||
Path.Combine(home, ".nvm", "versions", "node"),
|
||
Path.Combine(home, ".volta", "bin"),
|
||
Path.Combine(home, ".fnm"),
|
||
Path.Combine(home, ".npm-global", "bin")
|
||
});
|
||
}
|
||
|
||
foreach (var basePath in commonPaths)
|
||
{
|
||
if (Directory.Exists(basePath))
|
||
{
|
||
// Direct check (with and without .exe)
|
||
var directPath = Path.Combine(basePath, executableName);
|
||
if (File.Exists(directPath))
|
||
{
|
||
Debug.Log($"[MCP Setup] Detected {command}: {directPath}");
|
||
return directPath;
|
||
}
|
||
// Try without .exe on Windows (some tools don't have .exe)
|
||
if (isWindows)
|
||
{
|
||
var noExtPath = Path.Combine(basePath, command);
|
||
if (File.Exists(noExtPath))
|
||
{
|
||
Debug.Log($"[MCP Setup] Detected {command}: {noExtPath}");
|
||
return noExtPath;
|
||
}
|
||
}
|
||
|
||
// Search subdirectories (for version managers like NVM)
|
||
try
|
||
{
|
||
var subdirs = Directory.GetDirectories(basePath);
|
||
foreach (var subdir in subdirs)
|
||
{
|
||
var binPath = Path.Combine(subdir, "bin", command);
|
||
if (File.Exists(binPath))
|
||
{
|
||
Debug.Log($"[MCP Setup] Detected {command}: {binPath}");
|
||
return binPath;
|
||
}
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
|
||
// Search from PATH environment variable
|
||
var pathEnv = Environment.GetEnvironmentVariable("PATH");
|
||
if (!string.IsNullOrEmpty(pathEnv))
|
||
{
|
||
var separator = Application.platform == RuntimePlatform.WindowsEditor ? ';' : ':';
|
||
foreach (var path in pathEnv.Split(separator))
|
||
{
|
||
if (!string.IsNullOrEmpty(path))
|
||
{
|
||
var fullPath = Path.Combine(path.Trim(), executableName);
|
||
if (File.Exists(fullPath))
|
||
{
|
||
Debug.Log($"[MCP Setup] Detected {command} in PATH: {fullPath}");
|
||
return fullPath;
|
||
}
|
||
// Try without .exe
|
||
if (isWindows)
|
||
{
|
||
var noExtPath = Path.Combine(path.Trim(), command);
|
||
if (File.Exists(noExtPath))
|
||
{
|
||
Debug.Log($"[MCP Setup] Detected {command} in PATH: {noExtPath}");
|
||
return noExtPath;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Debug.LogWarning($"[MCP Setup] {command} not found in any known location");
|
||
return null; // Not found - return null to prevent hanging Process.Start
|
||
}
|
||
|
||
private string RunWhichCommand(string command)
|
||
{
|
||
try
|
||
{
|
||
var process = new Process
|
||
{
|
||
StartInfo = new ProcessStartInfo
|
||
{
|
||
FileName = "/usr/bin/which",
|
||
Arguments = command,
|
||
UseShellExecute = false,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
CreateNoWindow = true
|
||
}
|
||
};
|
||
|
||
process.Start();
|
||
var output = process.StandardOutput.ReadToEnd().Trim();
|
||
process.WaitForExit(3000);
|
||
if (!process.HasExited) process.Kill();
|
||
|
||
return process.ExitCode == 0 ? output : "";
|
||
}
|
||
catch
|
||
{
|
||
return "";
|
||
}
|
||
}
|
||
|
||
private string RunWhereCommand(string command)
|
||
{
|
||
try
|
||
{
|
||
var process = new Process
|
||
{
|
||
StartInfo = new ProcessStartInfo
|
||
{
|
||
FileName = "where.exe",
|
||
Arguments = command,
|
||
UseShellExecute = false,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
CreateNoWindow = true
|
||
}
|
||
};
|
||
|
||
process.Start();
|
||
var output = process.StandardOutput.ReadToEnd().Trim();
|
||
process.WaitForExit(3000);
|
||
if (!process.HasExited) process.Kill();
|
||
|
||
// whereは複数行返すことがある。最初の行を使う
|
||
var firstLine = output.Split('\n')[0].Trim();
|
||
return process.ExitCode == 0 && File.Exists(firstLine) ? firstLine : "";
|
||
}
|
||
catch
|
||
{
|
||
return "";
|
||
}
|
||
}
|
||
|
||
private async Task<bool> RunCommand(string command, string args, string workingDir = null)
|
||
{
|
||
try
|
||
{
|
||
var resolvedCommand = ResolveCommandPath(command);
|
||
Debug.Log($"[MCP Setup] Executing: {resolvedCommand} {args}");
|
||
|
||
var process = new Process
|
||
{
|
||
StartInfo = new ProcessStartInfo
|
||
{
|
||
FileName = resolvedCommand,
|
||
Arguments = args,
|
||
WorkingDirectory = workingDir ?? projectPath,
|
||
UseShellExecute = false,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
CreateNoWindow = true
|
||
}
|
||
};
|
||
|
||
// Set PATH environment variable appropriately
|
||
SetupEnvironmentPath(process.StartInfo);
|
||
|
||
process.Start();
|
||
|
||
var output = await process.StandardOutput.ReadToEndAsync();
|
||
var error = await process.StandardError.ReadToEndAsync();
|
||
|
||
process.WaitForExit();
|
||
|
||
if (!string.IsNullOrEmpty(output))
|
||
Debug.Log($"[MCP Setup] {output}");
|
||
|
||
if (!string.IsNullOrEmpty(error))
|
||
Debug.LogWarning($"[MCP Setup] {error}");
|
||
|
||
return process.ExitCode == 0;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[MCP Setup] Command error: {e.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private void RefreshEnvironmentPath()
|
||
{
|
||
// Reload environment variables
|
||
var path = Environment.GetEnvironmentVariable("PATH");
|
||
Environment.SetEnvironmentVariable("PATH", path, EnvironmentVariableTarget.Process);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Start MCP server in background
|
||
/// </summary>
|
||
private async Task StartMCPServerBackground(string npmPath)
|
||
{
|
||
try
|
||
{
|
||
var process = new Process
|
||
{
|
||
StartInfo = new ProcessStartInfo
|
||
{
|
||
FileName = npmPath,
|
||
Arguments = "start",
|
||
WorkingDirectory = mcpServerPath,
|
||
UseShellExecute = false,
|
||
RedirectStandardOutput = false,
|
||
RedirectStandardError = false,
|
||
CreateNoWindow = true,
|
||
WindowStyle = ProcessWindowStyle.Hidden
|
||
}
|
||
};
|
||
|
||
// Set PATH environment variable appropriately
|
||
SetupEnvironmentPath(process.StartInfo);
|
||
|
||
Debug.Log($"[MCP Setup] Starting MCP server in background: {npmPath} start");
|
||
process.Start();
|
||
|
||
// Wait a bit for process to start
|
||
await Task.Delay(1000);
|
||
|
||
mcpServerProcessId = process.Id;
|
||
Debug.Log($"[MCP Setup] MCP server process ID: {process.Id}");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[MCP Setup] Background startup error: {e.Message}");
|
||
|
||
// Fallback: start directly with node
|
||
try
|
||
{
|
||
var nodePath = ResolveCommandPath("node");
|
||
var nodeProcess = new Process
|
||
{
|
||
StartInfo = new ProcessStartInfo
|
||
{
|
||
FileName = nodePath,
|
||
Arguments = "index.js",
|
||
WorkingDirectory = mcpServerPath,
|
||
UseShellExecute = false,
|
||
RedirectStandardOutput = false,
|
||
RedirectStandardError = false,
|
||
CreateNoWindow = true,
|
||
WindowStyle = ProcessWindowStyle.Hidden
|
||
}
|
||
};
|
||
|
||
SetupEnvironmentPath(nodeProcess.StartInfo);
|
||
Debug.Log($"[MCP Setup] Fallback: {nodePath} index.js");
|
||
nodeProcess.Start();
|
||
await Task.Delay(1000);
|
||
}
|
||
catch (Exception nodeEx)
|
||
{
|
||
Debug.LogError($"[MCP Setup] Direct Node.js startup also failed: {nodeEx.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
private void CheckAIConfigurations()
|
||
{
|
||
// Check existence of AI configuration files
|
||
var configPath = Path.Combine(mcpServerPath, "ai-config");
|
||
if (Directory.Exists(configPath))
|
||
{
|
||
currentStatus.aiConfigurations["Claude"] = File.Exists(Path.Combine(configPath, "claude.json"));
|
||
currentStatus.aiConfigurations["Gemini"] = File.Exists(Path.Combine(configPath, "gemini.json"));
|
||
currentStatus.aiConfigurations["Copilot"] = File.Exists(Path.Combine(configPath, "copilot.json"));
|
||
}
|
||
}
|
||
|
||
private async Task InstallUnityPackage(string packageId, string version)
|
||
{
|
||
var manifestPath = Path.Combine(projectPath, "Packages", "manifest.json");
|
||
if (File.Exists(manifestPath))
|
||
{
|
||
var manifest = File.ReadAllText(manifestPath);
|
||
var packageLine = $"\"{packageId}\": \"{version}\"";
|
||
|
||
if (!manifest.Contains(packageId))
|
||
{
|
||
// Add to manifest.json
|
||
manifest = manifest.Replace("\"dependencies\": {", $"\"dependencies\": {{\n {packageLine},");
|
||
File.WriteAllText(manifestPath, manifest);
|
||
|
||
#if UNITY_EDITOR
|
||
AssetDatabase.Refresh();
|
||
#endif
|
||
}
|
||
}
|
||
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Start MCP server
|
||
/// </summary>
|
||
public async Task<bool> StartMCPServer()
|
||
{
|
||
try
|
||
{
|
||
Debug.Log("[MCP Setup] Checking for existing MCP server...");
|
||
|
||
// Don't start MCP server, just connect to existing server
|
||
bool serverExists = await CheckPortListening(8090);
|
||
|
||
if (serverExists)
|
||
{
|
||
Debug.Log("[MCP Setup] ✅ Found MCP server on port 8090 - Unity will connect as client");
|
||
mcpServerProcessId = -1; // External process so don't keep ID
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("[MCP Setup] ❌ No MCP server found on port 8090");
|
||
Debug.LogWarning("[MCP Setup] Please start Claude Desktop or another AI application first");
|
||
|
||
// Don't start MCP server on Unity side
|
||
return false;
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[MCP Setup] Failed to start server: {e.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private async Task<bool> CheckPortListening(int port)
|
||
{
|
||
try
|
||
{
|
||
if (Application.platform == RuntimePlatform.WindowsEditor)
|
||
{
|
||
var result = await CheckCommand("netstat", $"-an | findstr :{port}");
|
||
return !string.IsNullOrEmpty(result);
|
||
}
|
||
else
|
||
{
|
||
var result = await CheckCommand("lsof", $"-i :{port}");
|
||
return !string.IsNullOrEmpty(result);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private void SetupEnvironmentPath(ProcessStartInfo startInfo)
|
||
{
|
||
// Get current PATH
|
||
var currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||
|
||
// Paths to add
|
||
var additionalPaths = new[]
|
||
{
|
||
"/opt/homebrew/bin",
|
||
"/usr/local/bin",
|
||
"/usr/bin",
|
||
"/bin"
|
||
};
|
||
|
||
var pathList = new List<string>();
|
||
|
||
// Add additional paths first (to increase priority)
|
||
foreach (var path in additionalPaths)
|
||
{
|
||
if (Directory.Exists(path) && !pathList.Contains(path))
|
||
{
|
||
pathList.Add(path);
|
||
}
|
||
}
|
||
|
||
// Add existing PATH
|
||
if (!string.IsNullOrEmpty(currentPath))
|
||
{
|
||
var separator = Application.platform == RuntimePlatform.WindowsEditor ? ';' : ':';
|
||
foreach (var path in currentPath.Split(separator))
|
||
{
|
||
if (!string.IsNullOrEmpty(path.Trim()) && !pathList.Contains(path.Trim()))
|
||
{
|
||
pathList.Add(path.Trim());
|
||
}
|
||
}
|
||
}
|
||
|
||
// Set new PATH
|
||
var newPath = string.Join(Application.platform == RuntimePlatform.WindowsEditor ? ";" : ":", pathList);
|
||
startInfo.EnvironmentVariables["PATH"] = newPath;
|
||
|
||
Debug.Log($"[MCP Setup] Configured PATH: {newPath}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Clean up existing MCP servers
|
||
/// </summary>
|
||
private async Task CleanupExistingServers()
|
||
{
|
||
try
|
||
{
|
||
// First check if port is in use
|
||
bool needsCleanup = false;
|
||
|
||
for (int port = 3000; port <= 3010; port++)
|
||
{
|
||
if (await CheckPortListening(port))
|
||
{
|
||
needsCleanup = true;
|
||
Debug.Log($"[MCP Setup] Port {port} is in use");
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (await CheckPortListening(8090))
|
||
{
|
||
needsCleanup = true;
|
||
Debug.Log("[MCP Setup] Port 8090 is in use");
|
||
}
|
||
|
||
if (needsCleanup)
|
||
{
|
||
Debug.Log("[MCP Setup] Cleaning up existing server processes");
|
||
await StopMCPServer();
|
||
|
||
// Wait until port is released
|
||
await Task.Delay(2000);
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning($"[MCP Setup] Warning during cleanup: {e.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Stop MCP server
|
||
/// </summary>
|
||
public async Task<bool> StopMCPServer()
|
||
{
|
||
try
|
||
{
|
||
Debug.Log("[MCP Setup] Stopping MCP server...");
|
||
|
||
// First try to kill with saved process ID
|
||
if (mcpServerProcessId > 0)
|
||
{
|
||
try
|
||
{
|
||
var killByIdProcess = new Process
|
||
{
|
||
StartInfo = new ProcessStartInfo
|
||
{
|
||
FileName = "/bin/kill",
|
||
Arguments = $"-9 {mcpServerProcessId}",
|
||
UseShellExecute = false,
|
||
CreateNoWindow = true
|
||
}
|
||
};
|
||
killByIdProcess.Start();
|
||
killByIdProcess.WaitForExit();
|
||
Debug.Log($"[MCP Setup] Killed process {mcpServerProcessId}");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning($"[MCP Setup] Process ID kill error: {e.Message}");
|
||
}
|
||
}
|
||
|
||
// Kill node.js process with pkill just in case
|
||
await Task.Run(() =>
|
||
{
|
||
try
|
||
{
|
||
var killProcess = new Process
|
||
{
|
||
StartInfo = new ProcessStartInfo
|
||
{
|
||
FileName = "/usr/bin/pkill",
|
||
Arguments = "-f \"node.*index.js\"",
|
||
UseShellExecute = false,
|
||
CreateNoWindow = true,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true
|
||
}
|
||
};
|
||
killProcess.Start();
|
||
killProcess.WaitForExit();
|
||
|
||
Debug.Log("[MCP Setup] pkill command execution completed");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning($"[MCP Setup] Warning during pkill execution: {e.Message}");
|
||
}
|
||
});
|
||
|
||
// Wait until process completely terminates
|
||
await Task.Delay(1000);
|
||
|
||
// Verify that port is released
|
||
bool stillRunning = await CheckPortListening(3000);
|
||
if (!stillRunning)
|
||
{
|
||
Debug.Log("[MCP Setup] MCP server stopped successfully");
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("[MCP Setup] MCP server may still be running");
|
||
return false;
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[MCP Setup] Server stop error: {e.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
} |