diff --git a/.gitignore b/.gitignore index 75612dc8..f1d2443b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ CONTRIBUTING.md.meta .idea/ .vscode/ .aider* +.DS_Store* \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Data/McpClients.cs b/UnityMcpBridge/Editor/Data/McpClients.cs index d370df87..2360ab1b 100644 --- a/UnityMcpBridge/Editor/Data/McpClients.cs +++ b/UnityMcpBridge/Editor/Data/McpClients.cs @@ -43,6 +43,26 @@ public class McpClients mcpType = McpTypes.Cursor, configStatus = "Not Configured", }, + new() + { + name = "VSCode GitHub Copilot", + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Code", + "User", + "settings.json" + ), + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Library", + "Application Support", + "Code", + "User", + "settings.json" + ), + mcpType = McpTypes.VSCode, + configStatus = "Not Configured", + }, }; // Initialize status enums after construction diff --git a/UnityMcpBridge/Editor/Models/McpTypes.cs b/UnityMcpBridge/Editor/Models/McpTypes.cs index 913ed47c..0259b9c2 100644 --- a/UnityMcpBridge/Editor/Models/McpTypes.cs +++ b/UnityMcpBridge/Editor/Models/McpTypes.cs @@ -4,6 +4,7 @@ public enum McpTypes { ClaudeDesktop, Cursor, + VSCode, } } diff --git a/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs b/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs index 17c93e0b..07b7da3f 100644 --- a/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs @@ -8,13 +8,13 @@ namespace UnityMcpBridge.Editor.Windows // Editor window to display manual configuration instructions public class ManualConfigEditorWindow : EditorWindow { - private string configPath; - private string configJson; - private Vector2 scrollPos; - private bool pathCopied = false; - private bool jsonCopied = false; - private float copyFeedbackTimer = 0; - private McpClient mcpClient; + protected string configPath; + protected string configJson; + protected Vector2 scrollPos; + protected bool pathCopied = false; + protected bool jsonCopied = false; + protected float copyFeedbackTimer = 0; + protected McpClient mcpClient; public static void ShowWindow(string configPath, string configJson, McpClient mcpClient) { @@ -26,7 +26,7 @@ public static void ShowWindow(string configPath, string configJson, McpClient mc window.Show(); } - private void OnGUI() + protected virtual void OnGUI() { scrollPos = EditorGUILayout.BeginScrollView(scrollPos); @@ -245,7 +245,7 @@ private void OnGUI() EditorGUILayout.EndScrollView(); } - private void Update() + protected virtual void Update() { // Handle the feedback message timer if (copyFeedbackTimer > 0) diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index c794fb71..bf11f1fd 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -137,24 +137,75 @@ private void ConfigurationSection(McpClient mcpClient) // Create muted button style for Manual Setup GUIStyle mutedButtonStyle = new(buttonStyle); - if ( - GUILayout.Button( - $"Auto Configure {mcpClient.name}", - buttonStyle, - GUILayout.Height(28) - ) - ) + if (mcpClient.mcpType == McpTypes.VSCode) { - ConfigureMcpClient(mcpClient); - } + // Special handling for VSCode GitHub Copilot + if ( + GUILayout.Button( + "Auto Configure VSCode with GitHub Copilot", + buttonStyle, + GUILayout.Height(28) + ) + ) + { + ConfigureMcpClient(mcpClient); + } - if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28))) + if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28))) + { + // Show VSCode specific manual setup window + string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? mcpClient.windowsConfigPath + : mcpClient.linuxConfigPath; + + // Get the Python directory path + string pythonDir = FindPackagePythonDirectory(); + + // Create VSCode-specific configuration + var vscodeConfig = new + { + mcp = new + { + servers = new + { + unityMCP = new + { + command = "uv", + args = new[] { "--directory", pythonDir, "run", "server.py" } + } + } + } + }; + + JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); + + // Use the VSCodeManualSetupWindow directly since we're in the same namespace + VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson); + } + } + else { - // Get the appropriate config path based on OS - string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? mcpClient.windowsConfigPath - : mcpClient.linuxConfigPath; - ShowManualInstructionsWindow(configPath, mcpClient); + // Standard client buttons + if ( + GUILayout.Button( + $"Auto Configure {mcpClient.name}", + buttonStyle, + GUILayout.Height(28) + ) + ) + { + ConfigureMcpClient(mcpClient); + } + + if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28))) + { + // Get the appropriate config path based on OS + string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? mcpClient.windowsConfigPath + : mcpClient.linuxConfigPath; + ShowManualInstructionsWindow(configPath, mcpClient); + } } EditorGUILayout.Space(5); @@ -274,7 +325,7 @@ private void ToggleUnityBridge() isUnityBridgeRunning = !isUnityBridgeRunning; } - private string WriteToConfig(string pythonDir, string configPath) + private string WriteToConfig(string pythonDir, string configPath, McpClient mcpClient = null) { // Create configuration object for unityMCP McpConfigServer unityMCPConfig = new() @@ -303,14 +354,36 @@ private string WriteToConfig(string pythonDir, string configPath) dynamic existingConfig = JsonConvert.DeserializeObject(existingJson); existingConfig ??= new Newtonsoft.Json.Linq.JObject(); - // Ensure mcpServers object exists - if (existingConfig.mcpServers == null) + // Handle different client types with a switch statement + switch (mcpClient?.mcpType) { - existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject(); + case McpTypes.VSCode: + // VSCode specific configuration + // Ensure mcp object exists + if (existingConfig.mcp == null) + { + existingConfig.mcp = new Newtonsoft.Json.Linq.JObject(); + } + + // Ensure mcp.servers object exists + if (existingConfig.mcp.servers == null) + { + existingConfig.mcp.servers = new Newtonsoft.Json.Linq.JObject(); + } + break; + + default: + // Standard MCP configuration (Claude Desktop, Cursor, etc.) + // Ensure mcpServers object exists + if (existingConfig.mcpServers == null) + { + existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject(); + } + break; } - // Add/update unityMCP while preserving other servers - existingConfig.mcpServers.unityMCP = + // Add/update UnityMCP server in VSCode settings + existingConfig.mcp.servers.unityMCP = JsonConvert.DeserializeObject( JsonConvert.SerializeObject(unityMCPConfig) ); @@ -334,22 +407,49 @@ private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient { // Get the Python directory path using Package Manager API string pythonDir = FindPackagePythonDirectory(); - - // Create the manual configuration message - McpConfig jsonConfig = new() + string manualConfigJson; + + // Create common JsonSerializerSettings + JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + + // Use switch statement to handle different client types + switch (mcpClient.mcpType) { - mcpServers = new McpConfigServers - { - unityMCP = new McpConfigServer + case McpTypes.VSCode: + // Create VSCode-specific configuration with proper format + var vscodeConfig = new { - command = "uv", - args = new[] { "--directory", pythonDir, "run", "server.py" }, - }, - }, - }; - - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings); + mcp = new + { + servers = new + { + unityMCP = new + { + command = "uv", + args = new[] { "--directory", pythonDir, "run", "server.py" } + } + } + } + }; + manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); + break; + + default: + // Create standard MCP configuration for other clients + McpConfig jsonConfig = new() + { + mcpServers = new McpConfigServers + { + unityMCP = new McpConfigServer + { + command = "uv", + args = new[] { "--directory", pythonDir, "run", "server.py" }, + }, + }, + }; + manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings); + break; + } ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient); } @@ -450,7 +550,7 @@ private string ConfigureMcpClient(McpClient mcpClient) return "Manual Configuration Required"; } - string result = WriteToConfig(pythonDir, configPath); + string result = WriteToConfig(pythonDir, configPath, mcpClient); // Update the client status after successful configuration if (result == "Configured successfully") @@ -542,18 +642,42 @@ private void CheckMcpConfiguration(McpClient mcpClient) } string configJson = File.ReadAllText(configPath); - McpConfig config = JsonConvert.DeserializeObject(configJson); - - if (config?.mcpServers?.unityMCP != null) + string pythonDir = ServerInstaller.GetServerPath(); + + // Use switch statement to handle different client types, extracting common logic + string[] args = null; + bool configExists = false; + + switch (mcpClient.mcpType) { - string pythonDir = ServerInstaller.GetServerPath(); - if ( - pythonDir != null - && Array.Exists( - config.mcpServers.unityMCP.args, - arg => arg.Contains(pythonDir, StringComparison.Ordinal) - ) - ) + case McpTypes.VSCode: + dynamic config = JsonConvert.DeserializeObject(configJson); + + if (config?.mcp?.servers?.unityMCP != null) + { + // Extract args from VSCode config format + args = config.mcp.servers.unityMCP.args.ToObject(); + configExists = true; + } + break; + + default: + // Standard MCP configuration check for Claude Desktop, Cursor, etc. + McpConfig standardConfig = JsonConvert.DeserializeObject(configJson); + + if (standardConfig?.mcpServers?.unityMCP != null) + { + args = standardConfig.mcpServers.unityMCP.args; + configExists = true; + } + break; + } + + // Common logic for checking configuration status + if (configExists) + { + if (pythonDir != null && + Array.Exists(args, arg => arg.Contains(pythonDir, StringComparison.Ordinal))) { mcpClient.SetStatus(McpStatus.Configured); } diff --git a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs new file mode 100644 index 00000000..a0b78e2d --- /dev/null +++ b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs @@ -0,0 +1,295 @@ +using System.Runtime.InteropServices; +using UnityEditor; +using UnityEngine; +using UnityMcpBridge.Editor.Models; + +namespace UnityMcpBridge.Editor.Windows +{ + public class VSCodeManualSetupWindow : ManualConfigEditorWindow + { + public static new void ShowWindow(string configPath, string configJson) + { + var window = GetWindow("VSCode GitHub Copilot Setup"); + window.configPath = configPath; + window.configJson = configJson; + window.minSize = new Vector2(550, 500); + + // Create a McpClient for VSCode + window.mcpClient = new McpClient + { + name = "VSCode GitHub Copilot", + mcpType = McpTypes.VSCode + }; + + window.Show(); + } + + protected override void OnGUI() + { + scrollPos = EditorGUILayout.BeginScrollView(scrollPos); + + // Header with improved styling + EditorGUILayout.Space(10); + Rect titleRect = EditorGUILayout.GetControlRect(false, 30); + EditorGUI.DrawRect( + new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height), + new Color(0.2f, 0.2f, 0.2f, 0.1f) + ); + GUI.Label( + new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height), + "VSCode GitHub Copilot MCP Setup", + EditorStyles.boldLabel + ); + EditorGUILayout.Space(10); + + // Instructions with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + Rect headerRect = EditorGUILayout.GetControlRect(false, 24); + EditorGUI.DrawRect( + new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height), + new Color(0.1f, 0.1f, 0.1f, 0.2f) + ); + GUI.Label( + new Rect( + headerRect.x + 8, + headerRect.y + 4, + headerRect.width - 16, + headerRect.height + ), + "Setting up GitHub Copilot in VSCode with Unity MCP", + EditorStyles.boldLabel + ); + EditorGUILayout.Space(10); + + GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel) + { + margin = new RectOffset(10, 10, 5, 5), + }; + + EditorGUILayout.LabelField( + "1. Prerequisites", + EditorStyles.boldLabel + ); + EditorGUILayout.LabelField( + "• Ensure you have VSCode installed", + instructionStyle + ); + EditorGUILayout.LabelField( + "• Ensure you have GitHub Copilot extension installed in VSCode", + instructionStyle + ); + EditorGUILayout.LabelField( + "• Ensure you have a valid GitHub Copilot subscription", + instructionStyle + ); + EditorGUILayout.Space(5); + + EditorGUILayout.LabelField( + "2. Steps to Configure", + EditorStyles.boldLabel + ); + EditorGUILayout.LabelField( + "a) Open VSCode Settings (File > Preferences > Settings)", + instructionStyle + ); + EditorGUILayout.LabelField( + "b) Click on the 'Open Settings (JSON)' button in the top right", + instructionStyle + ); + EditorGUILayout.LabelField( + "c) Add the MCP configuration shown below to your settings.json file", + instructionStyle + ); + EditorGUILayout.LabelField( + "d) Save the file and restart VSCode", + instructionStyle + ); + EditorGUILayout.Space(5); + + EditorGUILayout.LabelField( + "3. VSCode settings.json location:", + EditorStyles.boldLabel + ); + + // Path section with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + string displayPath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + displayPath = System.IO.Path.Combine( + System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), + "Code", + "User", + "settings.json" + ); + } + else + { + displayPath = System.IO.Path.Combine( + System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), + "Library", + "Application Support", + "Code", + "User", + "settings.json" + ); + } + + // Store the path in the base class config path + if (string.IsNullOrEmpty(configPath)) + { + configPath = displayPath; + } + + // Prevent text overflow by allowing the text field to wrap + GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true }; + + EditorGUILayout.TextField( + displayPath, + pathStyle, + GUILayout.Height(EditorGUIUtility.singleLineHeight) + ); + + // Copy button with improved styling + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUIStyle copyButtonStyle = new(GUI.skin.button) + { + padding = new RectOffset(15, 15, 5, 5), + margin = new RectOffset(10, 10, 5, 5), + }; + + if ( + GUILayout.Button( + "Copy Path", + copyButtonStyle, + GUILayout.Height(25), + GUILayout.Width(100) + ) + ) + { + EditorGUIUtility.systemCopyBuffer = displayPath; + pathCopied = true; + copyFeedbackTimer = 2f; + } + + if ( + GUILayout.Button( + "Open File", + copyButtonStyle, + GUILayout.Height(25), + GUILayout.Width(100) + ) + ) + { + // Open the file using the system's default application + System.Diagnostics.Process.Start( + new System.Diagnostics.ProcessStartInfo + { + FileName = displayPath, + UseShellExecute = true, + } + ); + } + + if (pathCopied) + { + GUIStyle feedbackStyle = new(EditorStyles.label); + feedbackStyle.normal.textColor = Color.green; + EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60)); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(10); + + EditorGUILayout.LabelField( + "4. Add this configuration to your settings.json:", + EditorStyles.boldLabel + ); + + // JSON section with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // Improved text area for JSON with syntax highlighting colors + GUIStyle jsonStyle = new(EditorStyles.textArea) + { + font = EditorStyles.boldFont, + wordWrap = true, + }; + jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue + + // Draw the JSON in a text area with a taller height for better readability + EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200)); + + // Copy JSON button with improved styling + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + if ( + GUILayout.Button( + "Copy JSON", + copyButtonStyle, + GUILayout.Height(25), + GUILayout.Width(100) + ) + ) + { + EditorGUIUtility.systemCopyBuffer = configJson; + jsonCopied = true; + copyFeedbackTimer = 2f; + } + + if (jsonCopied) + { + GUIStyle feedbackStyle = new(EditorStyles.label); + feedbackStyle.normal.textColor = Color.green; + EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60)); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + EditorGUILayout.LabelField( + "5. After configuration:", + EditorStyles.boldLabel + ); + EditorGUILayout.LabelField( + "• Restart VSCode", + instructionStyle + ); + EditorGUILayout.LabelField( + "• GitHub Copilot will now be able to interact with your Unity project through the MCP protocol", + instructionStyle + ); + EditorGUILayout.LabelField( + "• Remember to have the Unity MCP Bridge running in Unity Editor", + instructionStyle + ); + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + // Close button at the bottom + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100))) + { + Close(); + } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.EndScrollView(); + } + + protected override void Update() + { + // Call the base implementation which handles the copy feedback timer + base.Update(); + } + } +} diff --git a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs.meta b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs.meta new file mode 100644 index 00000000..437ccab6 --- /dev/null +++ b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 377fe73d52cf0435fabead5f50a0d204 \ No newline at end of file