├── .gitignore ├── LICENSE ├── README.md ├── UnityMCPPlugin ├── Editor.meta ├── Editor │ ├── EditorCommandExecutor.cs │ ├── EditorStateReporter.cs │ ├── EditorUtilities.cs │ ├── ScriptTesterWindow.cs │ ├── UdonSharpHelper.cs │ ├── UnityMCP.Editor.asmdef │ ├── UnityMCP.Editor.asmdef.meta │ ├── UnityMCPConnection.cs │ ├── UnityMCPConnection.cs.meta │ ├── UnityMCPWindow.cs │ └── UnityMCPWindow.cs.meta ├── manifest.json ├── manifest.json.meta ├── package.json └── package.json.meta ├── diagram.mmd └── unity-mcp-server ├── Dockerfile ├── package.json ├── scripts └── copy-resources.js ├── smithery.yaml ├── src ├── communication │ └── UnityConnection.ts ├── index.ts ├── resources │ ├── TextResource.ts │ ├── index.ts │ ├── text │ │ ├── UdonSharp.md │ │ ├── VRCPickup.md │ │ └── VRChatWorldNotes.md │ └── types.ts └── tools │ ├── ExecuteEditorCommandTool.ts │ ├── GetEditorStateTool.ts │ ├── GetLogsTool.ts │ ├── index.ts │ └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Unity specific 2 | [Ll]ibrary/ 3 | [Tt]emp/ 4 | [Oo]bj/ 5 | [Bb]uild/ 6 | [Bb]uilds/ 7 | [Ll]ogs/ 8 | [Uu]ser[Ss]ettings/ 9 | 10 | # Unity3D generated meta files 11 | *.pidb.meta 12 | *.pdb.meta 13 | *.mdb.meta 14 | 15 | # Unity3D generated file on crash reports 16 | sysinfo.txt 17 | 18 | # Builds 19 | *.apk 20 | *.aab 21 | *.unitypackage 22 | *.app 23 | 24 | # Node.js / TypeScript 25 | node_modules/ 26 | dist/ 27 | *.tsbuildinfo 28 | .env 29 | .env.local 30 | .env.*.local 31 | 32 | # IDE / Editor files 33 | .vs/ 34 | .vscode/ 35 | *.swp 36 | *.swo 37 | .idea/ 38 | *.sublime-workspace 39 | *.sublime-project 40 | 41 | # OS generated 42 | .DS_Store 43 | .DS_Store? 44 | ._* 45 | .Spotlight-V100 46 | .Trashes 47 | ehthumbs.db 48 | Thumbs.db 49 | 50 | # Logs 51 | logs 52 | *.log 53 | npm-debug.log* 54 | yarn-debug.log* 55 | yarn-error.log* 56 | lerna-debug.log* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0) 2 | 3 | This work is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. 4 | To view a copy of this license, visit https://creativecommons.org/licenses/by-nc/4.0/ or send a letter to 5 | Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. 6 | 7 | You are free to: 8 | - Share — copy and redistribute the material in any medium or format 9 | - Adapt — remix, transform, and build upon the material 10 | 11 | Under the following terms: 12 | - Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. 13 | - NonCommercial — You may not use the material for commercial purposes. 14 | 15 | No additional restrictions — You may not apply legal terms or technological measures that legally restrict others 16 | from doing anything the license permits. 17 | 18 | Disclaimer: 19 | This license does not grant permission to use the trademarks, logos, or brand elements associated with the project. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Forked from [Arodoid/UnityMCP](https://github.com/Arodoid/UnityMCP) see that page for the 2 | original README.md 3 | 4 | ## About 5 | 6 | This repo has been extensively refactored from the source. I've been testing using Claude/MCP/Unity to create 7 | VRChat worlds. Claude has trouble getting UdonSharp scripts to compile so this repo supports 8 | MCP resources and helper scripts which improves it's success rate in building VRC worlds 9 | 10 | This repo also has a many general improvements that work with normal Unity development. Try it out. 11 | 12 | ## Improvements 13 | 14 | ### Command Execution 15 | - Changed how code is executed so that the LLM can define the usings, classes, and functions 16 | - Allows the LLM to execute more complex commands with multiple functions 17 | - Stack traces eat up a lot of context so just return the first line which is usually enough 18 | - Incorporated references for various modules: 19 | - .Net Standard 20 | - System.Core, System.IO 21 | - TextMeshPro assembly 22 | - VRChat assemblies 23 | - Unity Physics 24 | - A reference to MCPUnity itself so you can provide helper functions to MCP commands 25 | 26 | ### Unity Editor Integration 27 | - Added functionality to wait/retry when Unity is not connected to process commands 28 | - Changed `getEditorState` to run on demand instead of continuously 29 | - Implemented waiting for pending compilations when getting editor state and running commands 30 | - Revised GetAssets to retrieve all content from the Assets/ folder 31 | 32 | ### Manual Script Testing 33 | - Created a script tester for diagnosing C# script commands 34 | - Allows manually executing editor commands with detailed logging 35 | 36 | ### MCP Resources 37 | - Any files added to resources/text will be exposed as a MCP resource 38 | 39 | ### Performance 40 | - Fixed MCP window high CPU usage by only repainting when changes are detected 41 | - Enabled support for commands longer than 4KB in Unity 42 | - Reduced excessive debug logs during reconnection process 43 | 44 | ### Code Refactoring 45 | - Refactored Unity connection into its own dedicated file 46 | - Separated MCP server tools into individual files 47 | - With a common interface to make adding new tools easier 48 | - Split editor state reporting and command execution into their own files 49 | 50 | ### VRChat Specific features 51 | - Added a helper script that supports generating UdonSharp asset files from C# files 52 | 53 | ## How to Use 54 | 55 | - Build the MCP Server from unity-mcp-server/ 56 | - `npm run install` 57 | - `npm run build` 58 | 59 | - In Unity 60 | - Copy over the UnityMCPPlugin/ directory into your Assets folder 61 | - You should now see a UnityMCP menu in your project 62 | - Select `Debug Window` and dock by your projects 63 | 64 | - In Claude Desktop 65 | - Enable developer mode 66 | - Add the MCP server in File/Settings 67 | ``` 68 | { 69 | "mcpServers": { 70 | "unity": { 71 | "command": "node", 72 | "args": [ 73 | "C:\\git\\UnityMCP\\unity-mcp-server\\build\\index.js" 74 | ] 75 | } 76 | } 77 | } 78 | ``` 79 | - Verify in the UnityMCP Debug Window, the Connection Status is green/connected 80 | - Enter your prompt 81 | - Click the attach button below the prompt to add resource artifacts 82 | - Any file you add to the resources/text folder is exposed as a resource 83 | - You need to build the project and restart Claude for it to see new resources 84 | - Run your prompt 85 | - You should see scripts executing 86 | - If there are script errors you can diagnose them in Unity 87 | - The UnityMCP menu has a Script Tester where you can paste in scripts to run them manually 88 | 89 | ## License 90 | 91 | This project is licensed under the Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0). 92 | -------------------------------------------------------------------------------- /UnityMCPPlugin/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1b74ba37a53e9f84598f75bc2c9e8129 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /UnityMCPPlugin/Editor/EditorCommandExecutor.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using System; 4 | using System.Net.WebSockets; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using Newtonsoft.Json; 10 | using Microsoft.CSharp; 11 | using System.CodeDom.Compiler; 12 | using System.Threading.Tasks; 13 | 14 | namespace UnityMCP.Editor 15 | { 16 | public class EditorCommandExecutor 17 | { 18 | public class EditorCommandData 19 | { 20 | public string code { get; set; } 21 | } 22 | 23 | public static async Task ExecuteEditorCommand(ClientWebSocket webSocket, CancellationToken cancellationToken, string commandData) 24 | { 25 | var logs = new List(); 26 | var errors = new List(); 27 | var warnings = new List(); 28 | 29 | Application.logMessageReceived += LogHandler; 30 | 31 | try 32 | { 33 | var commandObj = JsonConvert.DeserializeObject(commandData); 34 | var code = commandObj.code; 35 | 36 | Debug.Log($"[UnityMCP] Executing code..."); 37 | // Execute the code directly in the Editor context 38 | try 39 | { 40 | // Execute the provided code 41 | var result = CompileAndExecute(code); 42 | 43 | Debug.Log($"[UnityMCP] Code executed"); 44 | 45 | // Send back detailed execution results 46 | var resultMessage = JsonConvert.SerializeObject(new 47 | { 48 | type = "commandResult", 49 | data = new 50 | { 51 | result = result, 52 | logs = logs, 53 | errors = errors, 54 | warnings = warnings, 55 | executionSuccess = true 56 | } 57 | }); 58 | 59 | var buffer = Encoding.UTF8.GetBytes(resultMessage); 60 | await webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); 61 | } 62 | catch (Exception e) 63 | { 64 | throw new Exception($"Failed to execute command: {e.Message}", e); 65 | } 66 | } 67 | catch (Exception e) 68 | { 69 | var firstStackLine = e.StackTrace?.Split('\n')?.FirstOrDefault() ?? ""; 70 | 71 | var error = $"[UnityMCP] Failed to execute editor command: {e.Message}\n{firstStackLine}"; 72 | Debug.LogError(error); 73 | 74 | // Send back error information 75 | var errorMessage = JsonConvert.SerializeObject(new 76 | { 77 | type = "commandResult", 78 | data = new 79 | { 80 | result = (object)null, 81 | logs = logs, 82 | errors = new List(errors) { error }, 83 | warnings = warnings, 84 | executionSuccess = false, 85 | errorDetails = new 86 | { 87 | message = e.Message, 88 | stackTrace = firstStackLine, 89 | type = e.GetType().Name 90 | } 91 | } 92 | }); 93 | var buffer = Encoding.UTF8.GetBytes(errorMessage); 94 | await webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); 95 | } 96 | finally 97 | { 98 | Application.logMessageReceived -= LogHandler; 99 | } 100 | 101 | void LogHandler(string message, string stackTrace, LogType type) 102 | { 103 | switch (type) 104 | { 105 | case LogType.Log: 106 | logs.Add(message); 107 | break; 108 | case LogType.Warning: 109 | warnings.Add(message); 110 | break; 111 | case LogType.Error: 112 | case LogType.Exception: 113 | var firstStackLine = stackTrace?.Split('\n')?.FirstOrDefault() ?? ""; 114 | errors.Add($"{message}\n{firstStackLine}"); 115 | break; 116 | } 117 | } 118 | } 119 | 120 | 121 | public static object CompileAndExecute(string code) 122 | { 123 | // Wait for any ongoing Unity compilation to finish first 124 | EditorUtilities.WaitForUnityCompilation(); 125 | 126 | // Use Mono's built-in compiler 127 | var options = new System.CodeDom.Compiler.CompilerParameters 128 | { 129 | GenerateInMemory = true, 130 | // Fixes error: The predefined type 'xxx' is defined multiple times. Using definition from 'mscorlib.dll' 131 | CompilerOptions = "/nostdlib+ /noconfig" 132 | }; 133 | 134 | // Track added assemblies to avoid duplicates 135 | HashSet addedAssemblies = new HashSet(); 136 | 137 | // Helper method to safely add assembly references 138 | void AddAssemblyReference(string assemblyPath) 139 | { 140 | if (!string.IsNullOrEmpty(assemblyPath) && !addedAssemblies.Contains(assemblyPath)) 141 | { 142 | options.ReferencedAssemblies.Add(assemblyPath); 143 | addedAssemblies.Add(assemblyPath); 144 | } 145 | } 146 | 147 | void AddAssemblyByName(string name) 148 | { 149 | try 150 | { 151 | var assembly = AppDomain.CurrentDomain.GetAssemblies() 152 | .FirstOrDefault(a => a.GetName().Name == name); 153 | if (assembly != null) 154 | { 155 | AddAssemblyReference(assembly.Location); 156 | } 157 | } 158 | catch (Exception e) 159 | { 160 | Debug.LogWarning($"[UnityMCP] Failed to add assembly {name}: {e.Message}"); 161 | } 162 | } 163 | 164 | try 165 | { 166 | options.CoreAssemblyFileName = typeof(object).Assembly.Location; 167 | 168 | // Add engine/editor core references 169 | AddAssemblyReference(typeof(UnityEngine.Object).Assembly.Location); 170 | AddAssemblyReference(typeof(UnityEditor.Editor).Assembly.Location); 171 | 172 | AddAssemblyReference(typeof(System.Linq.Enumerable).Assembly.Location); // Add System.Core for LINQ 173 | AddAssemblyReference(typeof(object).Assembly.Location); // Add mscorlib 174 | 175 | // Add this assembly so script can use utilities we provide 176 | AddAssemblyReference(typeof(UnityMCP.Editor.EditorCommandExecutor).Assembly.Location); 177 | 178 | // Add netstandard assembly 179 | var netstandardAssembly = AppDomain.CurrentDomain.GetAssemblies() 180 | .FirstOrDefault(a => a.GetName().Name == "netstandard"); 181 | if (netstandardAssembly != null) 182 | { 183 | AddAssemblyReference(netstandardAssembly.Location); 184 | } 185 | 186 | // Add common Unity modules 187 | var commonModules = new[] { 188 | "UnityEngine.AnimationModule", 189 | "UnityEngine.CoreModule", 190 | "UnityEngine.IMGUIModule", 191 | "UnityEngine.PhysicsModule", 192 | "UnityEngine.TerrainModule", 193 | "UnityEngine.TextRenderingModule", 194 | "UnityEngine.UIModule", 195 | "Unity.TextMeshPro", 196 | "Unity.TextMeshPro.Editor" 197 | }; 198 | 199 | foreach (var moduleName in commonModules) 200 | { 201 | AddAssemblyByName(moduleName); 202 | } 203 | 204 | // Add VRChat Udon and UdonSharp assemblies 205 | var vrchatAssemblies = new[] { 206 | "VRC.Udon", 207 | "VRC.Udon.Common", 208 | "VRC.Udon.Editor", 209 | "VRC.Udon.Serialization.OdinSerializer", 210 | "VRC.Udon.VM", 211 | "VRC.Udon.Wrapper", 212 | "UdonSharp.Editor", 213 | "UdonSharp.Runtime", 214 | "VRCSDK3", 215 | "VRCSDKBase", // Additional VRC SDK parts that might be needed 216 | }; 217 | 218 | foreach (var assemblyName in vrchatAssemblies) 219 | { 220 | AddAssemblyByName(assemblyName); 221 | } 222 | 223 | // Debug.Log("Added Assembly References:" + string.join(", ", options.ReferencedAssemblies)); 224 | } 225 | catch (Exception e) 226 | { 227 | Debug.LogWarning($"[UnityMCP] Assembly reference setup issue: {e.Message}"); 228 | } 229 | 230 | // Compile and execute 231 | using (var provider = new Microsoft.CSharp.CSharpCodeProvider()) 232 | { 233 | var results = provider.CompileAssemblyFromSource(options, code); 234 | if (results.Errors.HasErrors) 235 | { 236 | //Debug.LogError($"Assembly references: {string.Join(", ", options.ReferencedAssemblies)}"); 237 | foreach (CompilerError error in results.Errors) 238 | { 239 | Debug.LogError($"Error {error.ErrorNumber}: {error.ErrorText}, Line {error.Line}"); 240 | } 241 | var errors = string.Join("\n", results.Errors.Cast().Select(e => e.ErrorText)); 242 | throw new Exception($"Compilation failed:\n{errors}"); 243 | } 244 | 245 | var assembly = results.CompiledAssembly; 246 | var type = assembly.GetType("EditorCommand"); 247 | var method = type.GetMethod("Execute"); 248 | return method.Invoke(null, null); 249 | } 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /UnityMCPPlugin/Editor/EditorStateReporter.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using System; 4 | using System.Net.WebSockets; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using Newtonsoft.Json; 11 | 12 | namespace UnityMCP.Editor 13 | { 14 | public class EditorStateReporter 15 | { 16 | private string lastErrorMessage = ""; 17 | 18 | // New method to send editor state 19 | public async Task SendEditorState(ClientWebSocket webSocket, CancellationToken cancellationToken) 20 | { 21 | try 22 | { 23 | if (webSocket?.State == WebSocketState.Open) 24 | { 25 | var state = GetEditorState(); 26 | var message = JsonConvert.SerializeObject(new 27 | { 28 | type = "editorState", 29 | data = state 30 | }); 31 | var buffer = Encoding.UTF8.GetBytes(message); 32 | await webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); 33 | Debug.Log("[UnityMCP] Sent editor state upon request"); 34 | } 35 | else 36 | { 37 | Debug.LogWarning("[UnityMCP] Cannot send editor state - connection closed"); 38 | } 39 | } 40 | catch (Exception e) 41 | { 42 | Debug.LogError($"[UnityMCP] Error sending editor state: {e.Message}"); 43 | } 44 | } 45 | 46 | public object GetEditorState() 47 | { 48 | try 49 | { 50 | // Wait for any ongoing Unity compilation to finish first 51 | EditorUtilities.WaitForUnityCompilation(); 52 | 53 | var activeGameObjects = new List(); 54 | var selectedObjects = new List(); 55 | 56 | // Use FindObjectsByType instead of FindObjectsOfType 57 | var foundObjects = GameObject.FindObjectsByType(FindObjectsSortMode.None); 58 | if (foundObjects != null) 59 | { 60 | foreach (var obj in foundObjects) 61 | { 62 | if (obj != null && !string.IsNullOrEmpty(obj.name)) 63 | { 64 | activeGameObjects.Add(obj.name); 65 | } 66 | } 67 | } 68 | 69 | var selection = Selection.gameObjects; 70 | if (selection != null) 71 | { 72 | foreach (var obj in selection) 73 | { 74 | if (obj != null && !string.IsNullOrEmpty(obj.name)) 75 | { 76 | selectedObjects.Add(obj.name); 77 | } 78 | } 79 | } 80 | 81 | var currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); 82 | var sceneHierarchy = currentScene.IsValid() ? GetSceneHierarchy() : new List(); 83 | 84 | var projectStructure = new 85 | { 86 | scenes = GetSceneNames() ?? new string[0], 87 | assets = GetAssetPaths() ?? new string[0] 88 | }; 89 | 90 | return new 91 | { 92 | activeGameObjects, 93 | selectedObjects, 94 | playModeState = EditorApplication.isPlaying ? "Playing" : "Stopped", 95 | sceneHierarchy, 96 | projectStructure 97 | }; 98 | } 99 | catch (Exception e) 100 | { 101 | lastErrorMessage = $"Error getting editor state: {e.Message}"; 102 | Debug.LogError(lastErrorMessage); 103 | return new 104 | { 105 | activeGameObjects = new List(), 106 | selectedObjects = new List(), 107 | playModeState = "Unknown", 108 | sceneHierarchy = new List(), 109 | projectStructure = new { scenes = new string[0], assets = new string[0] } 110 | }; 111 | } 112 | } 113 | 114 | private object GetSceneHierarchy() 115 | { 116 | try 117 | { 118 | var roots = new List(); 119 | var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); 120 | 121 | if (scene.IsValid()) 122 | { 123 | var rootObjects = scene.GetRootGameObjects(); 124 | if (rootObjects != null) 125 | { 126 | foreach (var root in rootObjects) 127 | { 128 | if (root != null) 129 | { 130 | try 131 | { 132 | roots.Add(GetGameObjectHierarchy(root)); 133 | } 134 | catch (Exception e) 135 | { 136 | Debug.LogWarning($"[UnityMCP] Failed to get hierarchy for {root.name}: {e.Message}"); 137 | } 138 | } 139 | } 140 | } 141 | } 142 | 143 | return roots; 144 | } 145 | catch (Exception e) 146 | { 147 | lastErrorMessage = $"Error getting scene hierarchy: {e.Message}"; 148 | Debug.LogError(lastErrorMessage); 149 | return new List(); 150 | } 151 | } 152 | 153 | private object GetGameObjectHierarchy(GameObject obj) 154 | { 155 | try 156 | { 157 | if (obj == null) return null; 158 | 159 | var children = new List(); 160 | var transform = obj.transform; 161 | 162 | if (transform != null) 163 | { 164 | for (int i = 0; i < transform.childCount; i++) 165 | { 166 | try 167 | { 168 | var childTransform = transform.GetChild(i); 169 | if (childTransform != null && childTransform.gameObject != null) 170 | { 171 | var childHierarchy = GetGameObjectHierarchy(childTransform.gameObject); 172 | if (childHierarchy != null) 173 | { 174 | children.Add(childHierarchy); 175 | } 176 | } 177 | } 178 | catch (Exception e) 179 | { 180 | Debug.LogWarning($"[UnityMCP] Failed to process child {i} of {obj.name}: {e.Message}"); 181 | } 182 | } 183 | } 184 | 185 | return new 186 | { 187 | name = obj.name ?? "Unnamed", 188 | components = GetComponentNames(obj), 189 | children = children 190 | }; 191 | } 192 | catch (Exception e) 193 | { 194 | Debug.LogWarning($"[UnityMCP] Failed to get hierarchy for {(obj != null ? obj.name : "null")}: {e.Message}"); 195 | return null; 196 | } 197 | } 198 | 199 | private string[] GetComponentNames(GameObject obj) 200 | { 201 | try 202 | { 203 | if (obj == null) return new string[0]; 204 | 205 | var components = obj.GetComponents(); 206 | if (components == null) return new string[0]; 207 | 208 | var validComponents = new List(); 209 | foreach (var component in components) 210 | { 211 | try 212 | { 213 | if (component != null) 214 | { 215 | validComponents.Add(component.GetType().Name); 216 | } 217 | } 218 | catch (Exception e) 219 | { 220 | Debug.LogWarning($"[UnityMCP] Failed to get component name: {e.Message}"); 221 | } 222 | } 223 | 224 | return validComponents.ToArray(); 225 | } 226 | catch (Exception e) 227 | { 228 | Debug.LogWarning($"[UnityMCP] Failed to get component names for {(obj != null ? obj.name : "null")}: {e.Message}"); 229 | return new string[0]; 230 | } 231 | } 232 | 233 | private static string[] GetSceneNames() 234 | { 235 | var scenes = new List(); 236 | foreach (var scene in EditorBuildSettings.scenes) 237 | { 238 | scenes.Add(scene.path); 239 | } 240 | return scenes.ToArray(); 241 | } 242 | 243 | private static string[] GetAssetPaths() 244 | { 245 | var guids = AssetDatabase.FindAssets("", new[] { "Assets/" }); 246 | var paths = new string[guids.Length]; 247 | for (int i = 0; i < guids.Length; i++) 248 | { 249 | paths[i] = AssetDatabase.GUIDToAssetPath(guids[i]); 250 | } 251 | return paths; 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /UnityMCPPlugin/Editor/EditorUtilities.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using System; 4 | 5 | namespace UnityMCP.Editor 6 | { 7 | public static class EditorUtilities 8 | { 9 | /// 10 | /// Waits for Unity to finish any ongoing compilation or asset processing 11 | /// 12 | /// Maximum time to wait in seconds (0 means no timeout) 13 | /// True if compilation finished, false if timed out 14 | public static bool WaitForUnityCompilation(float timeoutSeconds = 60f) 15 | { 16 | if (!EditorApplication.isCompiling) 17 | return true; 18 | 19 | Debug.Log("[UnityMCP] Waiting for Unity to finish compilation..."); 20 | 21 | float startTime = Time.realtimeSinceStartup; 22 | bool complete = false; 23 | 24 | // Set up a waiter using EditorApplication.update 25 | EditorApplication.CallbackFunction waiter = null; 26 | waiter = () => 27 | { 28 | // Check if Unity finished compiling 29 | if (!EditorApplication.isCompiling) 30 | { 31 | EditorApplication.update -= waiter; 32 | complete = true; 33 | Debug.Log("[UnityMCP] Unity compilation completed"); 34 | } 35 | // Check for timeout if specified 36 | else if (timeoutSeconds > 0 && (Time.realtimeSinceStartup - startTime) > timeoutSeconds) 37 | { 38 | EditorApplication.update -= waiter; 39 | Debug.LogWarning($"[UnityMCP] Timed out waiting for Unity compilation after {timeoutSeconds} seconds"); 40 | } 41 | }; 42 | 43 | EditorApplication.update += waiter; 44 | 45 | // Force a synchronous wait since we're in an editor command context 46 | while (!complete && (timeoutSeconds <= 0 || (Time.realtimeSinceStartup - startTime) <= timeoutSeconds)) 47 | { 48 | System.Threading.Thread.Sleep(100); 49 | // Process events to keep the editor responsive 50 | if (EditorWindow.focusedWindow != null) 51 | { 52 | EditorWindow.focusedWindow.Repaint(); 53 | } 54 | } 55 | 56 | // Force a small delay to ensure any final processing is complete 57 | System.Threading.Thread.Sleep(500); 58 | 59 | return complete; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /UnityMCPPlugin/Editor/ScriptTesterWindow.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using System; 4 | using System.Collections.Generic; 5 | using UnityMCP.Editor; 6 | using Newtonsoft.Json; 7 | 8 | /** Used to diagnose why code LLM generates can't run */ 9 | public class ScriptTester : EditorWindow 10 | { 11 | private string scriptCode = "using UnityEngine;\nusing UnityEditor;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\npublic class EditorCommand\n{\n public static object Execute()\n {\n // Your code here\n return \"Hello from Script Tester!\";\n }\n}"; 12 | private Vector2 codeScrollPosition; 13 | private Vector2 resultScrollPosition; 14 | private Vector2 logsScrollPosition; // Added scroll position for logs 15 | private Vector2 mainScrollPosition; // Added scroll position for the entire window 16 | private string resultText = ""; 17 | private bool hasError = false; 18 | private List logs = new List(); 19 | private float codeEditorHeight = 400f; // Store the height of the code editor 20 | private bool isDraggingSplitter = false; // Track if user is dragging the splitter 21 | 22 | [MenuItem("UnityMCP/Script Tester")] 23 | public static void ShowWindow() 24 | { 25 | GetWindow("Script Tester"); 26 | } 27 | 28 | void OnGUI() 29 | { 30 | // Begin the main scroll view for the entire window content 31 | mainScrollPosition = EditorGUILayout.BeginScrollView(mainScrollPosition, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)); 32 | 33 | EditorGUILayout.Space(5); 34 | 35 | EditorGUILayout.LabelField("Enter C# Script:", EditorStyles.boldLabel); 36 | EditorGUILayout.HelpBox("Your script must contain a class named 'EditorCommand' with a static method 'Execute' that returns an object. Code surrounded by `` will be JSON parsed.", MessageType.Info); 37 | 38 | // Code input with syntax highlighting 39 | EditorGUILayout.BeginVertical(EditorStyles.helpBox); 40 | codeScrollPosition = EditorGUILayout.BeginScrollView(codeScrollPosition, GUILayout.Height(codeEditorHeight)); 41 | scriptCode = EditorGUILayout.TextArea(scriptCode, GUILayout.ExpandHeight(true)); 42 | EditorGUILayout.EndScrollView(); 43 | EditorGUILayout.EndVertical(); 44 | 45 | // Draw the splitter 46 | DrawSplitter(ref codeEditorHeight, 100f, 500f); 47 | 48 | EditorGUILayout.Space(10); 49 | 50 | // Execute button 51 | GUI.backgroundColor = new Color(0.7f, 0.9f, 0.7f); 52 | if (GUILayout.Button("Execute Script", GUILayout.Height(30))) 53 | { 54 | ExecuteScript(); 55 | } 56 | GUI.backgroundColor = Color.white; 57 | 58 | EditorGUILayout.Space(10); 59 | 60 | // Results area 61 | EditorGUILayout.LabelField("Results:", EditorStyles.boldLabel); 62 | 63 | EditorGUILayout.BeginVertical(EditorStyles.helpBox); 64 | resultScrollPosition = EditorGUILayout.BeginScrollView(resultScrollPosition, GUILayout.Height(75)); 65 | 66 | if (hasError) 67 | { 68 | EditorGUILayout.HelpBox(resultText, MessageType.Error); 69 | } 70 | else if (!string.IsNullOrEmpty(resultText)) 71 | { 72 | EditorGUILayout.SelectableLabel(resultText, EditorStyles.textField, GUILayout.ExpandHeight(true)); 73 | } 74 | 75 | EditorGUILayout.EndScrollView(); 76 | EditorGUILayout.EndVertical(); 77 | 78 | // Display logs if any 79 | if (logs.Count > 0) 80 | { 81 | EditorGUILayout.Space(10); 82 | EditorGUILayout.LabelField("Logs:", EditorStyles.boldLabel); 83 | 84 | foreach (var log in logs) 85 | { 86 | EditorGUILayout.HelpBox(log, MessageType.Info); 87 | } 88 | } 89 | 90 | // End the main scroll view 91 | EditorGUILayout.EndScrollView(); 92 | } 93 | 94 | private void ExecuteScript() 95 | { 96 | logs.Clear(); 97 | hasError = false; 98 | resultText = "Executing script..."; 99 | 100 | // Force immediate UI update before execution 101 | Repaint(); 102 | 103 | // Use delayCall instead of update to ensure UI has time to refresh 104 | // delayCall happens after all inspector redraws are complete 105 | EditorApplication.delayCall += ExecuteAfterRepaint; 106 | } 107 | 108 | private void ExecuteAfterRepaint() 109 | { 110 | // Remove the callback 111 | EditorApplication.delayCall -= ExecuteAfterRepaint; 112 | 113 | // Collect logs during execution 114 | Application.logMessageReceived += LogHandler; 115 | 116 | try 117 | { 118 | // Process the code if it appears to be from JSON 119 | string processedCode = scriptCode; 120 | if (scriptCode.StartsWith("`")) 121 | { 122 | processedCode = ProcessJsonStringCode(scriptCode); 123 | } 124 | 125 | // Execute the code using CSEditorHelper 126 | var result = UnityMCP.Editor.EditorCommandExecutor.CompileAndExecute(processedCode); 127 | 128 | // Format the result 129 | if (result != null) 130 | { 131 | resultText = "Result: " + result.ToString(); 132 | } 133 | else 134 | { 135 | resultText = "Result: null"; 136 | } 137 | } 138 | catch (Exception e) 139 | { 140 | hasError = true; 141 | resultText = $"Error: {e.Message}\n\nStack Trace:\n{e.StackTrace}"; 142 | } 143 | finally 144 | { 145 | Application.logMessageReceived -= LogHandler; 146 | } 147 | 148 | Repaint(); 149 | } 150 | 151 | // Process code string that might be from JSON with backticks 152 | private string ProcessJsonStringCode(string input) 153 | { 154 | if (string.IsNullOrEmpty(input)) 155 | return input; 156 | 157 | try 158 | { 159 | // Remove backticks if present 160 | if (input.StartsWith("`")) 161 | input = input.Trim('`'); 162 | 163 | // Sanitize input by joining lines that end with a comma and backslash 164 | string[] lines = input.Split(new[] { '\n', '\r' }, StringSplitOptions.None); 165 | List sanitizedLines = new List(); 166 | 167 | for (int i = 0; i < lines.Length; i++) 168 | { 169 | string currentLine = lines[i]; 170 | 171 | // Check if the current line ends with a comma and backslash 172 | while (i < lines.Length - 1 && currentLine.TrimEnd().EndsWith("\\")) 173 | { 174 | // Remove the trailing backslash and join with the next line 175 | currentLine = currentLine.TrimEnd().TrimEnd('\\') + lines[i + 1].TrimStart(); 176 | i++; // Skip the next line since we've joined it 177 | } 178 | 179 | sanitizedLines.Add(currentLine); 180 | } 181 | 182 | // Join the sanitized lines back together 183 | input = string.Join("\n", sanitizedLines); 184 | 185 | string unescaped = JsonConvert.DeserializeObject('"' + input + '"'); 186 | return unescaped; 187 | } 188 | catch (JsonException ex) 189 | { 190 | // Log the error but return the original input to avoid blocking execution 191 | Debug.LogWarning($"Failed to parse as JSON string: {ex.Message}"); 192 | return input; 193 | } 194 | } 195 | 196 | private void LogHandler(string message, string stackTrace, LogType type) 197 | { 198 | if (type == LogType.Log) 199 | { 200 | logs.Add(message); 201 | } 202 | else if (type == LogType.Warning) 203 | { 204 | logs.Add($"Warning: {message}"); 205 | } 206 | else if (type == LogType.Error || type == LogType.Exception) 207 | { 208 | logs.Add($"Error: {message}\n{stackTrace}"); 209 | } 210 | } 211 | 212 | // Draw a splitter that can be dragged to resize the element above it 213 | private void DrawSplitter(ref float heightToAdjust, float minHeight, float maxHeight) 214 | { 215 | EditorGUILayout.Space(2); 216 | 217 | // Draw the splitter handle 218 | Rect splitterRect = EditorGUILayout.GetControlRect(false, 5f); 219 | EditorGUI.DrawRect(splitterRect, new Color(0.5f, 0.5f, 0.5f, 0.5f)); 220 | 221 | // Change cursor when hovering over the splitter 222 | EditorGUIUtility.AddCursorRect(splitterRect, MouseCursor.ResizeVertical); 223 | 224 | // Handle splitter dragging 225 | Event e = Event.current; 226 | switch (e.type) 227 | { 228 | case EventType.MouseDown: 229 | if (splitterRect.Contains(e.mousePosition)) 230 | { 231 | isDraggingSplitter = true; 232 | e.Use(); 233 | } 234 | break; 235 | 236 | case EventType.MouseDrag: 237 | if (isDraggingSplitter) 238 | { 239 | heightToAdjust += e.delta.y; 240 | heightToAdjust = Mathf.Clamp(heightToAdjust, minHeight, maxHeight); 241 | e.Use(); 242 | Repaint(); 243 | } 244 | break; 245 | 246 | case EventType.MouseUp: 247 | if (isDraggingSplitter) 248 | { 249 | isDraggingSplitter = false; 250 | e.Use(); 251 | } 252 | break; 253 | } 254 | 255 | EditorGUILayout.Space(2); 256 | } 257 | } -------------------------------------------------------------------------------- /UnityMCPPlugin/Editor/UdonSharpHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEditor; 5 | using System.IO; 6 | using System; 7 | using UnityEditor.Compilation; 8 | 9 | namespace UnityMCP.VRChatUtils 10 | { 11 | public static class UdonSharpHelper 12 | { 13 | /// 14 | /// Creates a UdonSharp asset file for a given C# script 15 | /// There isn't an easy way I've found to do this programmatically so this method does it by manually creating an asset file 16 | /// 17 | /// Path to the cs file relative to the Assets folder 18 | /// Path to the created asset file 19 | public static string CreateAsset(string scriptPath) 20 | { 21 | // Validate script path 22 | if (string.IsNullOrEmpty(scriptPath) || !scriptPath.EndsWith(".cs")) 23 | { 24 | ThrowAndLog("Invalid script path: " + scriptPath); 25 | } 26 | 27 | // Get the script GUID 28 | string scriptGuid = AssetDatabase.AssetPathToGUID(scriptPath); 29 | if (string.IsNullOrEmpty(scriptGuid)) 30 | { 31 | ThrowAndLog("Could not find GUID for script at path: " + scriptPath); 32 | } 33 | 34 | // Verify UdonSharp program asset GUID 35 | string udonSharpProgramAssetGuid = GetUdonSharpProgramAssetGuid(); 36 | if (string.IsNullOrEmpty(udonSharpProgramAssetGuid)) 37 | { 38 | ThrowAndLog("UdonSharpProgramAsset GUID not found. Please ensure UdonSharp is installed correctly."); 39 | } 40 | 41 | // Determine the asset name and path 42 | string scriptName = Path.GetFileNameWithoutExtension(scriptPath); 43 | string assetName = scriptName; 44 | string scriptDirectory = Path.GetDirectoryName(scriptPath); 45 | string assetPath = Path.Combine(scriptDirectory, assetName + ".asset"); 46 | 47 | // Create the asset data 48 | var assetData = $@"%YAML 1.1 49 | %TAG !u! tag:unity3d.com,2011: 50 | --- !u!114 &11400000 51 | MonoBehaviour: 52 | m_ObjectHideFlags: 0 53 | m_CorrespondingSourceObject: {{fileID: 0}} 54 | m_PrefabInstance: {{fileID: 0}} 55 | m_PrefabAsset: {{fileID: 0}} 56 | m_GameObject: {{fileID: 0}} 57 | m_Enabled: 1 58 | m_EditorHideFlags: 0 59 | m_Script: {{fileID: 11500000, guid: {udonSharpProgramAssetGuid}, type: 3}} 60 | m_Name: {assetName} 61 | m_EditorClassIdentifier: 62 | sourceCsScript: {{fileID: 11500000, guid: {scriptGuid}, type: 3}} 63 | behaviourSyncMode: 0 64 | behaviourIDHeapVarName: 65 | variableNames: [] 66 | variableValues: []"; 67 | 68 | // Write the asset file 69 | File.WriteAllText(assetPath, assetData); 70 | Debug.Log($"Created UdonSharp asset at: {assetPath}"); 71 | 72 | // Refresh the AssetDatabase to detect the new file 73 | AssetDatabase.Refresh(); 74 | 75 | // Request a full script compilation 76 | CompilationPipeline.RequestScriptCompilation(); 77 | 78 | // Return the path to the created asset 79 | return assetPath; 80 | } 81 | 82 | /// 83 | /// Attempts to find the GUID of the UdonSharpProgramAsset script in the project 84 | /// 85 | /// GUID of the UdonSharpProgramAsset script or empty string if not found 86 | private static string GetUdonSharpProgramAssetGuid() 87 | { 88 | // Try to find UdonSharpProgramAsset.cs by name 89 | string[] guids = AssetDatabase.FindAssets("UdonSharpProgramAsset t:MonoScript"); 90 | foreach (string guid in guids) 91 | { 92 | var path = AssetDatabase.GUIDToAssetPath(guid); 93 | if (path.Contains("UdonSharpProgramAsset.cs")) 94 | { 95 | return guid; 96 | } 97 | } 98 | 99 | return string.Empty; 100 | } 101 | 102 | private static void ThrowAndLog(string message) 103 | { 104 | Debug.LogError(message); 105 | throw new Exception(message); 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /UnityMCPPlugin/Editor/UnityMCP.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UnityMCP.Editor", 3 | "rootNamespace": "UnityMCP.Editor", 4 | "references": [ 5 | "GUID:478a2357cc57436488a56e564b08d223" 6 | ], 7 | "includePlatforms": [ 8 | "Editor" 9 | ], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": false, 12 | "overrideReferences": true, 13 | "precompiledReferences": [ 14 | "Newtonsoft.Json.dll" 15 | ], 16 | "autoReferenced": true, 17 | "defineConstraints": [], 18 | "versionDefines": [], 19 | "noEngineReferences": false 20 | } -------------------------------------------------------------------------------- /UnityMCPPlugin/Editor/UnityMCP.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6f288460ac1cdeb4eb96c42a07655f77 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /UnityMCPPlugin/Editor/UnityMCPConnection.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using System; 4 | using System.Net.WebSockets; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using Newtonsoft.Json; 11 | using Microsoft.CSharp; 12 | using System.CodeDom.Compiler; 13 | 14 | namespace UnityMCP.Editor 15 | { 16 | [InitializeOnLoad] 17 | public class UnityMCPConnection 18 | { 19 | private static ClientWebSocket webSocket; 20 | private static bool isConnected = false; 21 | private static readonly Uri serverUri = new Uri("ws://localhost:8080"); 22 | private static string lastErrorMessage = ""; 23 | private static readonly Queue logBuffer = new Queue(); 24 | private static readonly int maxLogBufferSize = 1000; 25 | private static bool isLoggingEnabled = true; 26 | private static EditorStateReporter editorStateReporter; 27 | 28 | // Public properties for the debug window 29 | public static bool IsConnected => isConnected; 30 | public static Uri ServerUri => serverUri; 31 | public static string LastErrorMessage => lastErrorMessage; 32 | public static bool IsLoggingEnabled 33 | { 34 | get => isLoggingEnabled; 35 | set 36 | { 37 | isLoggingEnabled = value; 38 | if (value) 39 | { 40 | Application.logMessageReceived += HandleLogMessage; 41 | } 42 | else 43 | { 44 | Application.logMessageReceived -= HandleLogMessage; 45 | } 46 | } 47 | } 48 | 49 | private class LogEntry 50 | { 51 | public string Message { get; set; } 52 | public string StackTrace { get; set; } 53 | public LogType Type { get; set; } 54 | public DateTime Timestamp { get; set; } 55 | } 56 | 57 | // Public method to manually retry connection 58 | public static void RetryConnection() 59 | { 60 | Debug.Log("[UnityMCP] Manually retrying connection..."); 61 | ConnectToServer(); 62 | } 63 | private static readonly CancellationTokenSource cts = new CancellationTokenSource(); 64 | 65 | // Constructor called on editor startup 66 | static UnityMCPConnection() 67 | { 68 | // Start capturing logs before anything else 69 | Application.logMessageReceived += HandleLogMessage; 70 | isLoggingEnabled = true; 71 | 72 | Debug.Log("[UnityMCP] Plugin initialized"); 73 | EditorApplication.delayCall += () => 74 | { 75 | Debug.Log("[UnityMCP] Starting initial connection"); 76 | ConnectToServer(); 77 | }; 78 | EditorApplication.update += Update; 79 | } 80 | 81 | private static void HandleLogMessage(string message, string stackTrace, LogType type) 82 | { 83 | if (!isLoggingEnabled) return; 84 | 85 | var logEntry = new LogEntry 86 | { 87 | Message = message, 88 | StackTrace = stackTrace, 89 | Type = type, 90 | Timestamp = DateTime.UtcNow 91 | }; 92 | 93 | lock (logBuffer) 94 | { 95 | logBuffer.Enqueue(logEntry); 96 | while (logBuffer.Count > maxLogBufferSize) 97 | { 98 | logBuffer.Dequeue(); 99 | } 100 | } 101 | 102 | // Send log to server if connected 103 | if (isConnected && webSocket?.State == WebSocketState.Open) 104 | { 105 | SendLogToServer(logEntry); 106 | } 107 | } 108 | 109 | private static async void SendLogToServer(LogEntry logEntry) 110 | { 111 | try 112 | { 113 | var message = JsonConvert.SerializeObject(new 114 | { 115 | type = "log", 116 | data = new 117 | { 118 | message = logEntry.Message, 119 | stackTrace = logEntry.StackTrace, 120 | logType = logEntry.Type.ToString(), 121 | timestamp = logEntry.Timestamp 122 | } 123 | }); 124 | 125 | var buffer = Encoding.UTF8.GetBytes(message); 126 | await webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cts.Token); 127 | } 128 | catch (Exception e) 129 | { 130 | Debug.LogError($"[UnityMCP] Failed to send log to server: {e.Message}"); 131 | } 132 | } 133 | 134 | public static string[] GetRecentLogs(LogType[] types = null, int count = 100) 135 | { 136 | lock (logBuffer) 137 | { 138 | var logs = logBuffer.ToArray() 139 | .Where(log => types == null || types.Contains(log.Type)) 140 | .TakeLast(count) 141 | .Select(log => $"[{log.Timestamp:yyyy-MM-dd HH:mm:ss}] [{log.Type}] {log.Message}") 142 | .ToArray(); 143 | return logs; 144 | } 145 | } 146 | 147 | private static async void ConnectToServer() 148 | { 149 | if (webSocket != null && 150 | (webSocket.State == WebSocketState.Connecting || 151 | webSocket.State == WebSocketState.Open)) 152 | { 153 | // Debug.Log("[UnityMCP] Already connected or connecting"); 154 | return; 155 | } 156 | 157 | try 158 | { 159 | Debug.Log($"[UnityMCP] Attempting to connect to MCP Server at {serverUri}"); 160 | // Debug.Log($"[UnityMCP] Current Unity version: {Application.unityVersion}"); 161 | // Debug.Log($"[UnityMCP] Current platform: {Application.platform}"); 162 | 163 | webSocket = new ClientWebSocket(); 164 | webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(60); 165 | 166 | var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); 167 | using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, timeout.Token); 168 | 169 | await webSocket.ConnectAsync(serverUri, linkedCts.Token); 170 | isConnected = true; 171 | Debug.Log("[UnityMCP] Successfully connected to MCP Server"); 172 | StartReceiving(); 173 | 174 | // Initialize editor state reporter 175 | editorStateReporter = new EditorStateReporter(); 176 | } 177 | catch (OperationCanceledException) 178 | { 179 | lastErrorMessage = "[UnityMCP] Connection attempt timed out"; 180 | Debug.LogError(lastErrorMessage); 181 | isConnected = false; 182 | } 183 | catch (WebSocketException we) 184 | { 185 | lastErrorMessage = $"[UnityMCP] WebSocket error: {we.Message}\nDetails: {we.InnerException?.Message}"; 186 | Debug.LogError(lastErrorMessage); 187 | // Debug.LogError($"[UnityMCP] Stack trace: {we.StackTrace}"); 188 | isConnected = false; 189 | } 190 | catch (Exception e) 191 | { 192 | lastErrorMessage = $"[UnityMCP] Failed to connect to MCP Server: {e.Message}\nType: {e.GetType().Name}"; 193 | Debug.LogError(lastErrorMessage); 194 | // Debug.LogError($"[UnityMCP] Stack trace: {e.StackTrace}"); 195 | isConnected = false; 196 | } 197 | } 198 | 199 | private static float reconnectTimer = 0f; 200 | private static readonly float reconnectInterval = 5f; 201 | 202 | private static void Update() 203 | { 204 | if (!isConnected && webSocket?.State != WebSocketState.Open) 205 | { 206 | reconnectTimer += Time.deltaTime; 207 | if (reconnectTimer >= reconnectInterval) 208 | { 209 | Debug.Log("[UnityMCP] Attempting to reconnect..."); 210 | ConnectToServer(); 211 | reconnectTimer = 0f; 212 | } 213 | } 214 | } 215 | 216 | private static async void StartReceiving() 217 | { 218 | var buffer = new byte[1024 * 4]; 219 | var messageBuffer = new List(); 220 | try 221 | { 222 | while (webSocket.State == WebSocketState.Open) 223 | { 224 | var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cts.Token); 225 | 226 | if (result.MessageType == WebSocketMessageType.Text) 227 | { 228 | // Add received data to our message buffer 229 | messageBuffer.AddRange(new ArraySegment(buffer, 0, result.Count)); 230 | 231 | // If this is the end of the message, process it 232 | if (result.EndOfMessage) 233 | { 234 | var message = Encoding.UTF8.GetString(messageBuffer.ToArray()); 235 | await HandleMessage(message); 236 | messageBuffer.Clear(); 237 | } 238 | // Otherwise, continue receiving the rest of the message 239 | } 240 | else if (result.MessageType == WebSocketMessageType.Close) 241 | { 242 | // Handle close message 243 | await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cts.Token); 244 | isConnected = false; 245 | Debug.Log("[UnityMCP] WebSocket connection closed normally"); 246 | break; 247 | } 248 | } 249 | } 250 | catch (Exception e) 251 | { 252 | Debug.LogError($"Error receiving message: {e.Message}"); 253 | isConnected = false; 254 | } 255 | } 256 | 257 | private static async Task HandleMessage(string message) 258 | { 259 | try 260 | { 261 | var data = JsonConvert.DeserializeObject>(message); 262 | switch (data["type"].ToString()) 263 | { 264 | case "executeEditorCommand": 265 | await EditorCommandExecutor.ExecuteEditorCommand(webSocket, cts.Token, data["data"].ToString()); 266 | break; 267 | case "getEditorState": 268 | await editorStateReporter.SendEditorState(webSocket, cts.Token); 269 | break; 270 | } 271 | } 272 | catch (Exception e) 273 | { 274 | Debug.LogError($"Error handling message: {e.Message}"); 275 | } 276 | } 277 | } 278 | } -------------------------------------------------------------------------------- /UnityMCPPlugin/Editor/UnityMCPConnection.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 89dbb1c9d43c5734ba1707eb307a8cab -------------------------------------------------------------------------------- /UnityMCPPlugin/Editor/UnityMCPWindow.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using System; 4 | 5 | namespace UnityMCP.Editor 6 | { 7 | public class UnityMCPWindow : EditorWindow 8 | { 9 | // State tracking for efficient repainting 10 | private bool previousConnectionState; 11 | private string previousErrorMessage; 12 | 13 | [MenuItem("UnityMCP/Debug Window", false, 1)] 14 | public static void ShowWindow() 15 | { 16 | GetWindow("UnityMCP Debug"); 17 | } 18 | 19 | void OnEnable() 20 | { 21 | // Initialize state tracking 22 | previousConnectionState = UnityMCPConnection.IsConnected; 23 | previousErrorMessage = UnityMCPConnection.LastErrorMessage; 24 | 25 | // Register for updates 26 | EditorApplication.update += CheckForChanges; 27 | } 28 | 29 | void OnDisable() 30 | { 31 | // Clean up 32 | EditorApplication.update -= CheckForChanges; 33 | } 34 | 35 | void CheckForChanges() 36 | { 37 | // Only repaint if something we're displaying has changed 38 | bool connectionChanged = previousConnectionState != UnityMCPConnection.IsConnected; 39 | bool errorChanged = previousErrorMessage != UnityMCPConnection.LastErrorMessage; 40 | 41 | if (connectionChanged || errorChanged) 42 | { 43 | // Update cached values 44 | previousConnectionState = UnityMCPConnection.IsConnected; 45 | previousErrorMessage = UnityMCPConnection.LastErrorMessage; 46 | 47 | Repaint(); 48 | } 49 | } 50 | 51 | void OnGUI() 52 | { 53 | try 54 | { 55 | EditorGUILayout.Space(10); 56 | 57 | GUILayout.Label("UnityMCP Debug", EditorStyles.boldLabel); 58 | EditorGUILayout.Space(5); 59 | 60 | // Connection status with background 61 | EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); 62 | EditorGUILayout.LabelField("Connection Status:", GUILayout.Width(120)); 63 | GUI.color = UnityMCPConnection.IsConnected ? Color.green : Color.red; 64 | EditorGUILayout.LabelField(UnityMCPConnection.IsConnected ? "Connected" : "Disconnected", EditorStyles.boldLabel); 65 | GUI.color = Color.white; 66 | EditorGUILayout.EndHorizontal(); 67 | 68 | EditorGUILayout.Space(5); 69 | 70 | // Server URI with background 71 | EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); 72 | EditorGUILayout.LabelField("MCP Server URI:", GUILayout.Width(120)); 73 | EditorGUILayout.SelectableLabel(UnityMCPConnection.ServerUri.ToString(), EditorStyles.textField, GUILayout.Height(20)); 74 | EditorGUILayout.EndHorizontal(); 75 | 76 | EditorGUILayout.Space(10); 77 | 78 | // Retry button - make it more prominent 79 | if (GUILayout.Button("Retry Connection", GUILayout.Height(30))) 80 | { 81 | UnityMCPConnection.RetryConnection(); 82 | } 83 | 84 | EditorGUILayout.Space(10); 85 | 86 | // Last error message if any 87 | if (!string.IsNullOrEmpty(UnityMCPConnection.LastErrorMessage)) 88 | { 89 | EditorGUILayout.LabelField("Last Error:", EditorStyles.boldLabel); 90 | EditorGUILayout.HelpBox(UnityMCPConnection.LastErrorMessage, MessageType.Error); 91 | } 92 | } 93 | catch (Exception e) 94 | { 95 | EditorGUILayout.HelpBox($"Error in debug window: {e.Message}", MessageType.Error); 96 | } 97 | } 98 | 99 | // Remove the old Update method as we're using EditorApplication.update instead 100 | } 101 | } -------------------------------------------------------------------------------- /UnityMCPPlugin/Editor/UnityMCPWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4c97646158d1f9f4fb925319ebd08c4d -------------------------------------------------------------------------------- /UnityMCPPlugin/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity MCP Integration", 3 | "version": "1.0.0", 4 | "documentationUrl": "https://github.com/anthropic/unity-mcp", 5 | "changelogUrl": "https://github.com/anthropic/unity-mcp/changelog.md", 6 | "licensesUrl": "https://github.com/anthropic/unity-mcp/license.md", 7 | "dependencies": { 8 | "com.unity.nuget.newtonsoft-json": "3.0.2" 9 | }, 10 | "keywords": [ 11 | "claude", 12 | "ai", 13 | "mcp", 14 | "editor" 15 | ] 16 | } -------------------------------------------------------------------------------- /UnityMCPPlugin/manifest.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e4a1edc47a6499c489aed017beb6742a 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /UnityMCPPlugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.claude.unity-mcp", 3 | "version": "1.0.0", 4 | "displayName": "Unity MCP Integration", 5 | "description": "Unity Editor integration with Claude AI via Model Context Protocol", 6 | "unity": "2020.3", 7 | "dependencies": { 8 | "com.unity.nuget.newtonsoft-json": "3.2.1" 9 | }, 10 | "keywords": [ 11 | "claude", 12 | "ai", 13 | "mcp", 14 | "editor" 15 | ], 16 | "author": { 17 | "name": "Claude AI", 18 | "email": "support@anthropic.com", 19 | "url": "https://www.anthropic.com" 20 | }, 21 | "samples": [], 22 | "type": "tool", 23 | "hideInEditor": false 24 | } 25 | -------------------------------------------------------------------------------- /UnityMCPPlugin/package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 955e5fe4b7956ac4386830a6d161ebc0 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /diagram.mmd: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant User 3 | participant MCPClient as MCP Client 4 | participant MCPServer as MCP Server
(index.ts) 5 | participant UnityConn as Unity Connection
(UnityMCPConnection.cs) 6 | participant UnityEditor as Unity Editor 7 | 8 | User->>MCPClient: Issue command 9 | MCPClient->>MCPServer: Call tool
(CallToolRequestSchema) 10 | 11 | alt Tool: execute_editor_command 12 | MCPServer->>UnityConn: Send WebSocket message
(type: "executeEditorCommand") 13 | Note over MCPServer: Creates commandResultPromise
in UnityMCPServer class 14 | 15 | UnityConn->>UnityConn: HandleMessage()
identifies command type 16 | UnityConn->>UnityEditor: CSEditorHelper.ExecuteCommand()
or ExecuteSimpleCommand() 17 | Note over UnityConn: Compiles and executes C# code 18 | 19 | UnityEditor-->>UnityConn: Return execution result 20 | UnityConn-->>MCPServer: WebSocket response
(type: "commandResult") 21 | 22 | Note over MCPServer: handleUnityMessage()
resolves commandResultPromise 23 | else Tool: get_editor_state 24 | MCPServer->>MCPServer: Return current editorState 25 | Note over MCPServer: No direct Unity communication
State updated via background polling 26 | else Tool: get_logs 27 | MCPServer->>MCPServer: filterLogs() 28 | Note over MCPServer: Returns logs from buffer
Unity sends logs continuously 29 | end 30 | 31 | MCPServer-->>MCPClient: Return tool result 32 | MCPClient-->>User: Display result 33 | 34 | loop Background Communication 35 | UnityConn->>MCPServer: Send editor state updates
(type: "editorState") 36 | Note over UnityConn: StartSendingEditorState()
every 1 second 37 | 38 | UnityConn->>MCPServer: Send log messages
(type: "log") 39 | Note over UnityConn: HandleLogMessage()
on Application.logMessageReceived 40 | end -------------------------------------------------------------------------------- /unity-mcp-server/Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:lts-alpine 3 | 4 | # Create app directory 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json if available 8 | COPY package*.json ./ 9 | 10 | # Install dependencies (without running prepare scripts to avoid build script issues) 11 | RUN npm install --ignore-scripts 12 | 13 | # Copy the rest of the application source 14 | COPY . . 15 | 16 | # Build the project 17 | RUN npm run build 18 | 19 | # Expose port 8080 (if needed, though not strictly required for stdio-based communication, but unity-mcp-server uses websockets on port 8080 per readme) 20 | EXPOSE 8080 21 | 22 | # Start the MCP server 23 | CMD ["node", "build/index.js"] 24 | -------------------------------------------------------------------------------- /unity-mcp-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UnityMCP", 3 | "version": "0.1.0", 4 | "description": "MCP for talking with Unity Editor via a Unity plugin + an MCP server", 5 | "private": true, 6 | "type": "module", 7 | "bin": { 8 | "UnityMCP": "./build/index.js" 9 | }, 10 | "files": [ 11 | "build" 12 | ], 13 | "scripts": { 14 | "build": "tsc && node scripts/copy-resources.js && node --input-type=module -e \"import { chmod } from 'fs/promises'; await chmod('build/index.js', '755');\"", 15 | "watch": "tsc --watch", 16 | "inspector": "npx @modelcontextprotocol/inspector build/index.js" 17 | }, 18 | "dependencies": { 19 | "@modelcontextprotocol/sdk": "0.6.0", 20 | "ws": "^8.16.0" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^20.11.24", 24 | "@types/ws": "^8.18.0", 25 | "typescript": "^5.3.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /unity-mcp-server/scripts/copy-resources.js: -------------------------------------------------------------------------------- 1 | import { mkdir, cp } from 'fs/promises'; 2 | import { dirname } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import path from 'path'; 5 | 6 | // Get current directory 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | const rootDir = path.resolve(__dirname, '..'); 10 | 11 | const src = path.join(rootDir, 'src', 'resources', 'text'); 12 | const dest = path.join(rootDir, 'build', 'resources', 'text'); 13 | 14 | async function copyResources() { 15 | try { 16 | // Ensure the destination directory exists 17 | await mkdir(dirname(dest), { recursive: true }); 18 | 19 | // Copy files recursively 20 | await cp(src, dest, { recursive: true, force: true }); 21 | 22 | console.log(`Successfully copied ${src} to ${dest}`); 23 | } catch (err) { 24 | console.error('Error copying resources:', err); 25 | process.exit(1); 26 | } 27 | } 28 | 29 | copyResources(); 30 | -------------------------------------------------------------------------------- /unity-mcp-server/smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | {} 8 | commandFunction: 9 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 10 | |- 11 | (config) => ({ 12 | command: 'node', 13 | args: ['build/index.js'] 14 | }) 15 | exampleConfig: {} 16 | -------------------------------------------------------------------------------- /unity-mcp-server/src/communication/UnityConnection.ts: -------------------------------------------------------------------------------- 1 | import { WebSocket, WebSocketServer } from "ws"; 2 | import { CommandResult, resolveCommandResult } from "../tools/ExecuteEditorCommandTool.js"; 3 | import { LogEntry } from "../tools/index.js"; 4 | import { resolveUnityEditorState, UnityEditorState } from "../tools/GetEditorStateTool.js"; 5 | 6 | export class UnityConnection { 7 | private wsServer: WebSocketServer; 8 | private connection: WebSocket | null = null; 9 | 10 | private logBuffer: LogEntry[] = []; 11 | private readonly maxLogBufferSize = 1000; 12 | 13 | // Event callbacks 14 | private onLogReceived: ((entry: LogEntry) => void) | null = null; 15 | 16 | constructor(port: number = 8080) { 17 | this.wsServer = new WebSocketServer({ port }); 18 | this.setupWebSocket(); 19 | } 20 | 21 | private setupWebSocket() { 22 | console.error("[Unity MCP] WebSocket server starting on port 8080"); 23 | 24 | this.wsServer.on("listening", () => { 25 | console.error( 26 | "[Unity MCP] WebSocket server is listening for connections", 27 | ); 28 | }); 29 | 30 | this.wsServer.on("error", (error) => { 31 | console.error("[Unity MCP] WebSocket server error:", error); 32 | }); 33 | 34 | this.wsServer.on("connection", (ws: WebSocket) => { 35 | console.error("[Unity MCP] Unity Editor connected"); 36 | this.connection = ws; 37 | 38 | ws.on("message", (data: Buffer) => { 39 | try { 40 | const message = JSON.parse(data.toString()); 41 | console.error("[Unity MCP] Received message:", message.type); 42 | this.handleUnityMessage(message); 43 | } catch (error) { 44 | console.error("[Unity MCP] Error handling message:", error); 45 | } 46 | }); 47 | 48 | ws.on("error", (error) => { 49 | console.error("[Unity MCP] WebSocket error:", error); 50 | }); 51 | 52 | ws.on("close", () => { 53 | console.error("[Unity MCP] Unity Editor disconnected"); 54 | this.connection = null; 55 | }); 56 | }); 57 | } 58 | 59 | private handleUnityMessage(message: any) { 60 | switch (message.type) { 61 | case "commandResult": 62 | resolveCommandResult(message.data as CommandResult); 63 | break; 64 | 65 | case "editorState": 66 | resolveUnityEditorState(message.data as UnityEditorState); 67 | break; 68 | 69 | case "log": 70 | this.handleLogMessage(message.data); 71 | if (this.onLogReceived) { 72 | this.onLogReceived(message.data); 73 | } 74 | break; 75 | 76 | default: 77 | console.error("[Unity MCP] Unknown message type:", message.type); 78 | } 79 | } 80 | 81 | private handleLogMessage(logEntry: LogEntry) { 82 | // Add to buffer, removing oldest if at capacity 83 | this.logBuffer.push(logEntry); 84 | if (this.logBuffer.length > this.maxLogBufferSize) { 85 | this.logBuffer.shift(); 86 | } 87 | } 88 | 89 | // Public API 90 | public isConnected(): boolean { 91 | return this.connection !== null; 92 | } 93 | 94 | public getLogBuffer(): LogEntry[] { 95 | return [...this.logBuffer]; 96 | } 97 | 98 | public setOnLogReceived(callback: (entry: LogEntry) => void): void { 99 | this.onLogReceived = callback; 100 | } 101 | 102 | public sendMessage(type: string, data: any): void { 103 | if (this.connection) { 104 | this.connection.send(JSON.stringify({ type, data })); 105 | } else { 106 | console.error( 107 | "[Unity MCP] Cannot send message: Unity Editor not connected", 108 | ); 109 | } 110 | } 111 | 112 | public async waitForConnection(timeoutMs: number = 60000): Promise { 113 | if (this.connection) return true; 114 | 115 | return new Promise((resolve) => { 116 | const timeout = setTimeout(() => resolve(false), timeoutMs); 117 | 118 | const connectionHandler = () => { 119 | clearTimeout(timeout); 120 | this.wsServer.off("connection", connectionHandler); 121 | resolve(true); 122 | }; 123 | 124 | this.wsServer.on("connection", connectionHandler); 125 | }); 126 | } 127 | 128 | public close(): void { 129 | if (this.connection) { 130 | this.connection.close(); 131 | this.connection = null; 132 | } 133 | this.wsServer.close(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /unity-mcp-server/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { 5 | CallToolRequestSchema, 6 | ErrorCode, 7 | ListResourcesRequestSchema, 8 | ListToolsRequestSchema, 9 | McpError, 10 | ReadResourceRequestSchema, 11 | } from "@modelcontextprotocol/sdk/types.js"; 12 | import { UnityConnection } from "./communication/UnityConnection.js"; 13 | import { getAllResources, ResourceContext } from "./resources/index.js"; 14 | import { getAllTools, ToolContext } from "./tools/index.js"; 15 | 16 | class UnityMCPServer { 17 | private server: Server; 18 | private unityConnection: UnityConnection; 19 | private initialized = false; 20 | 21 | constructor() { 22 | // Initialize MCP Server 23 | this.server = new Server( 24 | { 25 | name: "unity-mcp-server", 26 | version: "0.1.0", 27 | }, 28 | { 29 | capabilities: { 30 | tools: {}, 31 | resources: {}, 32 | }, 33 | }, 34 | ); 35 | 36 | // Initialize WebSocket Server for Unity communication 37 | this.unityConnection = new UnityConnection(8080); 38 | 39 | // Error handling 40 | this.server.onerror = (error) => console.error("[MCP Error]", error); 41 | process.on("SIGINT", async () => { 42 | await this.cleanup(); 43 | process.exit(0); 44 | }); 45 | } 46 | 47 | /** Initialize the server asynchronously */ 48 | async initialize() { 49 | if (this.initialized) return; 50 | 51 | await this.setupResources(); 52 | this.setupTools(); 53 | 54 | this.initialized = true; 55 | } 56 | 57 | /** Optional resources the user can include in Claude Desktop to give additional context to the LLM */ 58 | private async setupResources() { 59 | const resources = await getAllResources(); 60 | 61 | // Set up the resource request handler 62 | this.server.setRequestHandler( 63 | ListResourcesRequestSchema, 64 | async (request) => { 65 | return { 66 | resources: resources.map((resource) => resource.getDefinition()), 67 | }; 68 | }, 69 | ); 70 | 71 | // Read resource contents 72 | this.server.setRequestHandler( 73 | ReadResourceRequestSchema, 74 | async (request) => { 75 | const uri = request.params.uri; 76 | const resource = resources.find((r) => r.getDefinition().uri === uri); 77 | 78 | if (!resource) { 79 | throw new McpError( 80 | ErrorCode.MethodNotFound, 81 | `Resource not found: ${uri}. Available resources: ${resources 82 | .map((r) => r.getDefinition().uri) 83 | .join(", ")}`, 84 | ); 85 | } 86 | 87 | const resourceContext: ResourceContext = { 88 | unityConnection: this.unityConnection, 89 | // Add any other context properties needed 90 | }; 91 | 92 | const content = await resource.getContents(resourceContext); 93 | 94 | return { 95 | contents: [ 96 | { 97 | uri, 98 | mimeType: resource.getDefinition().mimeType, 99 | text: content, 100 | }, 101 | ], 102 | }; 103 | }, 104 | ); 105 | } 106 | 107 | private setupTools() { 108 | const tools = getAllTools(); 109 | 110 | // List available tools with comprehensive documentation 111 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 112 | tools: tools.map((tool) => tool.getDefinition()), 113 | })); 114 | 115 | // Handle tool calls with enhanced validation and error handling 116 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 117 | const { name, arguments: args } = request.params; 118 | 119 | // Find the requested tool 120 | const tool = tools.find((t) => t.getDefinition().name === name); 121 | 122 | // Validate tool exists with helpful error message 123 | if (!tool) { 124 | const availableTools = tools.map((t) => t.getDefinition().name); 125 | throw new McpError( 126 | ErrorCode.MethodNotFound, 127 | `Unknown tool: ${name}. Available tools are: ${availableTools.join( 128 | ", ", 129 | )}`, 130 | ); 131 | } 132 | 133 | // Try executing with retry logic for connection issues 134 | let retryCount = 0; 135 | const maxRetries = 5; 136 | const retryDelay = 5000; // 5 seconds 137 | 138 | while (true) { 139 | // Verify Unity connection with detailed error message 140 | if (!this.unityConnection.isConnected()) { 141 | if (retryCount < maxRetries) { 142 | retryCount++; 143 | console.error(`Unity Editor not connected. Retrying in 5 seconds... (${retryCount}/${maxRetries})`); 144 | await new Promise(resolve => setTimeout(resolve, retryDelay)); 145 | continue; 146 | } 147 | throw new McpError( 148 | ErrorCode.InternalError, 149 | "Unity Editor is not connected. Please ensure the Unity Editor is running and the UnityMCP window is open.", 150 | ); 151 | } 152 | 153 | // Create context object for tool execution 154 | const toolContext: ToolContext = { 155 | unityConnection: this.unityConnection, 156 | logBuffer: this.unityConnection.getLogBuffer(), 157 | }; 158 | 159 | // Execute the tool 160 | return await tool.execute(args, toolContext); 161 | } 162 | }); 163 | } 164 | 165 | private async cleanup() { 166 | this.unityConnection.close(); 167 | await this.server.close(); 168 | } 169 | 170 | async run() { 171 | await this.initialize(); 172 | 173 | const transport = new StdioServerTransport(); 174 | await this.server.connect(transport); 175 | console.error("Unity MCP server running on stdio"); 176 | 177 | // Wait for WebSocket server to be ready 178 | await new Promise((resolve) => { 179 | setTimeout(resolve, 100); // Small delay to ensure WebSocket server is initialized 180 | }); 181 | } 182 | } 183 | 184 | const server = new UnityMCPServer(); 185 | server.run().catch(console.error); 186 | -------------------------------------------------------------------------------- /unity-mcp-server/src/resources/TextResource.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | import { Resource, ResourceContext, ResourceDefinition } from "./types.js"; 4 | 5 | export class TextResource implements Resource { 6 | private filePath: string; 7 | private fileName: string; 8 | 9 | constructor(filePath: string) { 10 | this.filePath = filePath; 11 | this.fileName = path.basename(filePath); 12 | } 13 | 14 | getDefinition(): ResourceDefinition { 15 | return { 16 | uri: `file:///${this.fileName}`, 17 | name: this.fileName, 18 | mimeType: "text/plain", 19 | description: `Text file: ${this.fileName}`, 20 | }; 21 | } 22 | 23 | async getContents(context: ResourceContext): Promise { 24 | try { 25 | return await fs.readFile(this.filePath, "utf8"); 26 | } catch (error) { 27 | console.error(`Error reading text file ${this.filePath}:`, error); 28 | return `Error loading text file: ${this.fileName}`; 29 | } 30 | } 31 | } 32 | 33 | export async function loadTextResources( 34 | directoryPath: string, 35 | ): Promise { 36 | try { 37 | const textFiles = await fs.readdir(directoryPath); 38 | 39 | return textFiles.map( 40 | (file) => new TextResource(path.join(directoryPath, file)), 41 | ); 42 | } catch (error) { 43 | console.error("Error loading text resources:", error); 44 | return []; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /unity-mcp-server/src/resources/index.ts: -------------------------------------------------------------------------------- 1 | import path, { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { loadTextResources } from "./TextResource.js"; 4 | import { Resource } from "./types.js"; 5 | 6 | export * from "./types.js"; 7 | 8 | // Get the directory name of the current module 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | 12 | // Path to text resources relative to the built code 13 | const textResourceDir = path.join(__dirname, "text"); 14 | 15 | export async function getAllResources(): Promise { 16 | const staticResources: Resource[] = [ 17 | // Add static resources here as they are implemented 18 | ]; 19 | 20 | // Load dynamic text resources 21 | const textResources = await loadTextResources(textResourceDir); 22 | 23 | return [...staticResources, ...textResources]; 24 | } 25 | -------------------------------------------------------------------------------- /unity-mcp-server/src/resources/text/UdonSharp.md: -------------------------------------------------------------------------------- 1 | # UdonSharp 2 | 3 | ## A compiler for compiling C# to Udon assembly 4 | 5 | UdonSharp is a compiler that compiles C# to Udon assembly. UdonSharp is not currently conformant to any version of the C# language specification, so there are many things that are not implemented or will not work. 6 | 7 | ## C# features supported 8 | - Flow control 9 | - Supports: `if` `else` `while` `for` `do` `foreach` `switch` `return` `break` `continue` `ternary operator (condition ? true : false)` `??` 10 | - Implicit and explicit type conversions 11 | - Arrays and array indexers 12 | - All builtin arithmetic operators 13 | - Conditional short circuiting `(true || CheckIfTrue())` will not execute CheckIfTrue() 14 | - `typeof()` 15 | - Extern methods with out or ref parameters (such as many variants of `Physics.Raycast()`) 16 | - User defined methods with parameters and return values, supports out/ref, extension methods, and `params` 17 | - User defined properties 18 | - Static user methods 19 | - UdonSharpBehaviour inheritence, virtual methods, etc. 20 | - Unity/Udon event callbacks with arguments. For instance, registering a OnPlayerJoined event with a VRCPlayerApi argument is valid. 21 | - String interpolation 22 | - Field initializers 23 | - Jagged arrays 24 | - Referencing other custom UdonSharpBehaviour classes, accessing fields, and calling methods on them 25 | - Recursive method calls are supported via the `[RecursiveMethod]` attribute 26 | 27 | ## Differences from regular Unity C# to note 28 | - For the best experience making UdonSharp scripts, make your scripts inherit from `UdonSharpBehaviour` instead of `MonoBehaviour` 29 | - If you need to call `GetComponent()` you will need to use `(UdonBehaviour)GetComponent(typeof(UdonBehaviour))` at the moment since the generic get component is not exposed for UdonBehaviour yet. `GetComponent()` works for other Unity component types though. 30 | - Udon currently only supports array `[]` collections and by extension UdonSharp only supports arrays at the moment. It looks like they might support `List` at some point, but it is not there yet. 31 | - Field initilizers are evaluated at compile time, if you have any init logic that depends on other objects in the scene you should use Start for this. 32 | - Use the `UdonSynced` attribute on fields that you want to sync. 33 | - Numeric casts are checked for overflow due to UdonVM limitations 34 | - The internal type of variables returned by `.GetType()` will not always match what you may expect since U# abstracts some types in order to make them work in Udon. For instance, any jagged array type will return a type of `object[]` instead of something like `int[][]` for a 2D int jagged array. 35 | 36 | ## Udon bugs that affect U# 37 | - Mutating methods on structs do not modify the struct (this can be seen on things like calling Normalize() on a Vector3) https://vrchat.canny.io/vrchat-udon-closed-alpha-bugs/p/raysetorigin-and-raysetdirection-not-working -------------------------------------------------------------------------------- /unity-mcp-server/src/resources/text/VRCPickup.md: -------------------------------------------------------------------------------- 1 | Allows objects to be picked up, held and used by players. 2 | 3 | ## Proximity Rules 4 | 5 | :::note 6 | 7 | All of the rules described in this section also apply to "Interactables", i.e. UdonBehaviours that have an `Interact` event (they will also show a "Proximity" slider in the Inspector). 8 | ::: 9 | 10 | The "Proximity" value defines from how far away your pickup will be grabbable. It is given in meters, aka. "Unity units", where the side-length of one default Cube equals 1 unit. 11 | 12 | There are 2 mechanisms of grabbing where the Proximity value will be in play: 13 | 14 | - **Raycast:** If you are far away from a pickup, or you are running in desktop mode, pickups will be highlighted if the "laser" coming out of your hands (in VR) or your head (desktop) intersects the collider on a pickup object. The distance calculation to compare against Proximity is different in VR and Desktop mode: 15 | - VR: The distance between the origin of the laser (i.e. your hand controllers) and the impact point on the collider, in meters 16 | - Desktop: The distance between the origin of the laser (i.e. your head or main camera position) minus an "extra reach" value to compensate for being unable to move your hands forward. This is an approximate value that also takes your avatar scale into account ("longer arms"). Since it is subtracted from the distance, it will allow you to grab objects that are technically outside of the "Proximity" range, but could be grabbed by moving your arms while standing in this position in VR. 17 | - **Hover (VR only):** If your hands are close enough to an object, pickups will be highlighted even if a ray in the direction of the UI laser would not intersect the object. This allows more natural "grabbing" of objects. The distance of reach is a sphere centered on your hands with a radius of 0.4 meters times your avatar scale (note that this value is not directly comparable to the "avatar scaling" system available to users, although changing the avatar scale that way can affect the reach). 18 | - The "Proximity" value is still compared against the raw distance between your hand and the collider on the pickup, meaning the "reach distance" described is only an upper bound for the closeness at which "Hover" mode will engage. 19 | - For example, setting the Proximity to 0 will require the hand to be _inside_ the collider for the pickup to be highlighted (it will still be grabbable in Desktop mode however, because of the "extra reach" desktop users get to compensate). 20 | - As an advanced technique, you may want to adjust the Proximity value based on the data provided via the [avatar scaling system](/worlds/udon/players/player-avatar-scaling). 21 | 22 | ## Requires: 23 | 24 | - Rigidbody 25 | - Collider 26 | 27 | | Parameter | Description | 28 | | - | - | 29 | | Momentum Transfer Method | This defines how the collision force will be added to the other object which was hit, using Rigidbody.AddForceAtPosition.
Note: the force will only be added if 'AllowCollisionTransfer' is on. | 30 | | Disallow Theft | If other users are allowed to take the pickup out of someone else's grip | 31 | | Exact Gun | The position object will be held if set to Exact Gun | 32 | | Exact Grip | The position object will be held if set to Exact Grip. | 33 | | Allow Manipulation When Equipped | Should the user be able to manipulate the pickup while the pickup is held if using a controller? | 34 | | Orientation | What way the object will be held. | 35 | | Auto Hold | Should the pickup remain in the user's hand after they let go of the grab button.
- Auto Detect: Automatically detects what to do
- Yes: After the grab button is released the pickup remains in the hand until the drop button is pressed and released
- No: After the grab button is released the pickup is let go
Note: This only applies to control schemes which lack an independent "Use" input from "Grab", such as Desktop, Vive, and Vive-like input systems. This does not apply to Quest, Quest-like, and other input systems with a defined trigger. | 36 | | Use Text | Text that appears when the user has an object equipped, prompting them to "fire" the object.
Requires "Auto Hold" to be set to "Yes". | 37 | | Throw Velocity Boost Min Speed | How fast the object needs to move to be thrown. | 38 | | Throw Velocity Boost Scale | How much throwing should scale. Higher = faster thrown while lower means slower throw speed. | 39 | | Pickupable | Can you pick up the pickup? | 40 | | Proximity | The maximum distance this pickup is reachable from. See section above for more details. | -------------------------------------------------------------------------------- /unity-mcp-server/src/resources/text/VRChatWorldNotes.md: -------------------------------------------------------------------------------- 1 | # VRChat World Unity Project Notes 2 | 3 | ## Project Structure 4 | - Put all new assets in the `Assets/Game/` folder 5 | - Before making edits, examine existing components to understand what's already in place 6 | 7 | ## Development Best Practices 8 | - Use multiple `execute_editor_commands` instead of trying to do everything in one command 9 | - Unity and UdonSharp are complex environments with many potential issues 10 | - Break down your tasks into smaller, more manageable commands 11 | 12 | ## Useful Commands 13 | 14 | ### Creating UdonSharp Asset Files 15 | To create the corresponding asset file for UdonSharp scripts so you can attach them to components in Unity: 16 | ```csharp 17 | UnityMCP.VRChatUtils.UdonSharpHelper.CreateAsset("Assets/Game/yourScript.cs"); 18 | ``` 19 | 20 | ### Compiling the Project 21 | To compile the entire project and check for errors in the code: 22 | ```csharp 23 | CompilationPipeline.RequestScriptCompilation(); 24 | ``` 25 | 26 | ## Rendering Text in Udon 27 | 28 | Legacy Unity TextMesh (or UI.Text) components can't be used directly in Udon because many of their methods and properties aren't exposed. Instead: 29 | 30 | - Use TextMeshPro-based components that are supported by VRChat's Udon 31 | - If TextMeshPro is not installed, it can be added via the Unity Package Manager 32 | 33 | ## Code Guidelines 34 | - Don't end lines with `\\` to continue a string to the next line as that is not valid C# 35 | - If unsure about implementation, ask for clarification -------------------------------------------------------------------------------- /unity-mcp-server/src/resources/types.ts: -------------------------------------------------------------------------------- 1 | import { UnityConnection } from "../communication/UnityConnection.js"; 2 | 3 | export interface ResourceDefinition { 4 | uri: string; 5 | name: string; 6 | mimeType: string; 7 | description?: string; 8 | } 9 | 10 | export interface ResourceContext { 11 | unityConnection: UnityConnection; 12 | // Add any other context properties needed by resources 13 | } 14 | 15 | export interface Resource { 16 | getDefinition(): ResourceDefinition; 17 | getContents(context: ResourceContext): Promise; 18 | } 19 | -------------------------------------------------------------------------------- /unity-mcp-server/src/tools/ExecuteEditorCommandTool.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; 2 | import { 3 | Tool, 4 | ToolContext, 5 | ToolDefinition, 6 | } from "./types.js"; 7 | 8 | export interface CommandResult { 9 | result: any; 10 | logs: string[]; 11 | errors: string[]; 12 | warnings: string[]; 13 | executionSuccess: boolean; 14 | errorDetails?: { 15 | message: string; 16 | stackTrace: string; 17 | type: string; 18 | }; 19 | } 20 | 21 | export interface CommandResultHandler { 22 | resolve: (value: CommandResult) => void; 23 | reject: (reason?: any) => void; 24 | } 25 | 26 | // Command state management 27 | let commandResultPromise: CommandResultHandler | null = null; 28 | let commandStartTime: number | null = null; 29 | 30 | // New method to resolve the command result - called when results arrive from Unity 31 | export function resolveCommandResult(result: CommandResult): void { 32 | if (commandResultPromise) { 33 | commandResultPromise.resolve(result); 34 | commandResultPromise = null; 35 | } 36 | } 37 | 38 | export class ExecuteEditorCommandTool implements Tool { 39 | getDefinition(): ToolDefinition { 40 | return { 41 | name: "execute_editor_command", 42 | description: 43 | "Execute arbitrary C# code file within the Unity Editor context. This powerful tool allows for direct manipulation of the Unity Editor, GameObjects, components, and project assets using the Unity Editor API.", 44 | category: "Editor Control", 45 | tags: ["unity", "editor", "command", "c#", "scripting"], 46 | inputSchema: { 47 | type: "object", 48 | properties: { 49 | code: { 50 | type: "string", 51 | description: `C# code file to execute in the Unity Editor context. 52 | The code has access to all UnityEditor and UnityEngine APIs. 53 | Include any necessary using directives at the top of the code. 54 | The code must have a EditorCommand class with a static Execute method that returns an object.`, 55 | minLength: 1, 56 | }, 57 | }, 58 | required: ["code"], 59 | additionalProperties: false, 60 | }, 61 | returns: { 62 | type: "object", 63 | description: 64 | "Returns the execution result and any logs generated during execution", 65 | format: 'JSON object containing "result" and "logs" fields', 66 | }, 67 | errorHandling: { 68 | description: "Common error scenarios and their handling:", 69 | scenarios: [ 70 | { 71 | error: "Compilation error", 72 | handling: "Returns compilation error details in logs", 73 | }, 74 | { 75 | error: "Runtime exception", 76 | handling: "Returns exception details and stack trace", 77 | }, 78 | { 79 | error: "Timeout", 80 | handling: "Command execution timeout after 5 seconds", 81 | }, 82 | ], 83 | }, 84 | examples: [ 85 | { 86 | description: "Center selected object", 87 | input: { 88 | code: `using UnityEngine; 89 | using UnityEditor; 90 | using System; 91 | using System.Linq; 92 | using System.Collections.Generic; 93 | using System.IO; 94 | using System.Reflection; 95 | 96 | public class EditorCommand 97 | { 98 | public static object Execute() 99 | { 100 | Selection.activeGameObject.transform.position = Vector3.zero; 101 | EditorApplication.isPlaying = !EditorApplication.isPlaying; 102 | return ""Success""; 103 | } 104 | }`, 105 | }, 106 | output: 107 | '{ "result": true, "logs": ["[UnityMCP] Command executed successfully"] }', 108 | }, 109 | ], 110 | }; 111 | } 112 | 113 | async execute(args: any, context: ToolContext) { 114 | // Validate code parameter 115 | if (!args?.code) { 116 | throw new McpError( 117 | ErrorCode.InvalidParams, 118 | "The code parameter is required", 119 | ); 120 | } 121 | 122 | if (typeof args.code !== "string") { 123 | throw new McpError( 124 | ErrorCode.InvalidParams, 125 | "The code parameter must be a string", 126 | ); 127 | } 128 | 129 | if (args.code.trim().length === 0) { 130 | throw new McpError( 131 | ErrorCode.InvalidParams, 132 | "The code parameter cannot be empty", 133 | ); 134 | } 135 | 136 | try { 137 | // Clear previous logs and set command start time 138 | const startLogIndex = context.logBuffer.length; 139 | commandStartTime = Date.now(); 140 | 141 | // Send command to Unity 142 | context.unityConnection!.sendMessage("executeEditorCommand", { 143 | code: args.code, 144 | }); 145 | 146 | // Wait for result with enhanced timeout handling 147 | const timeoutMs = 60_000; 148 | const result = await Promise.race([ 149 | new Promise((resolve, reject) => { 150 | commandResultPromise = { resolve, reject }; 151 | }), 152 | new Promise((_, reject) => 153 | setTimeout( 154 | () => 155 | reject( 156 | new Error( 157 | `Command execution timed out after ${ 158 | timeoutMs / 1000 159 | } seconds. This may indicate a long-running operation or an issue with the Unity Editor.`, 160 | ), 161 | ), 162 | timeoutMs, 163 | ), 164 | ), 165 | ]); 166 | 167 | // Get logs that occurred during command execution 168 | const commandLogs = context.logBuffer 169 | .slice(startLogIndex) 170 | .filter((log) => log.message.includes("[UnityMCP]")); 171 | 172 | // Calculate execution time 173 | const executionTime = Date.now() - (commandStartTime || 0); 174 | 175 | return { 176 | content: [ 177 | { 178 | type: "text", 179 | text: JSON.stringify( 180 | { 181 | result, 182 | logs: commandLogs, 183 | executionTime: `${executionTime}ms`, 184 | status: "success", 185 | }, 186 | null, 187 | 2, 188 | ), 189 | }, 190 | ], 191 | }; 192 | } catch (error) { 193 | // Enhanced error handling with specific error types 194 | if (error instanceof Error) { 195 | if (error.message.includes("timed out")) { 196 | throw new McpError(ErrorCode.InternalError, error.message); 197 | } 198 | 199 | // Check for common Unity-specific errors 200 | if (error.message.includes("NullReferenceException")) { 201 | throw new McpError( 202 | ErrorCode.InvalidParams, 203 | "The code attempted to access a null object. Please check that all GameObject references exist.", 204 | ); 205 | } 206 | 207 | if (error.message.includes("CompileError")) { 208 | throw new McpError( 209 | ErrorCode.InvalidParams, 210 | "C# compilation error. Please check the syntax of your code.", 211 | ); 212 | } 213 | } 214 | 215 | // Generic error fallback 216 | throw new McpError( 217 | ErrorCode.InternalError, 218 | `Failed to execute command: ${ 219 | error instanceof Error ? error.message : "Unknown error" 220 | }`, 221 | ); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /unity-mcp-server/src/tools/GetEditorStateTool.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; 2 | import { Tool, ToolContext, ToolDefinition } from "./types.js"; 3 | 4 | export interface UnityEditorState { 5 | activeGameObjects: string[]; 6 | selectedObjects: string[]; 7 | playModeState: string; 8 | sceneHierarchy: any; 9 | projectStructure: { 10 | scenes?: string[]; 11 | assets?: string[]; 12 | [key: string]: string[] | undefined; 13 | }; 14 | } 15 | 16 | export interface UnityEditorStateHandler { 17 | resolve: (value: UnityEditorState) => void; 18 | reject: (reason?: any) => void; 19 | } 20 | 21 | // Command state management 22 | let unityEditorStatePromise: UnityEditorStateHandler | null = null; 23 | let unityEditorStateTime: number | null = null; 24 | 25 | // New method to resolve the command result - called when results arrive from Unity 26 | export function resolveUnityEditorState(result: UnityEditorState): void { 27 | if (unityEditorStatePromise) { 28 | unityEditorStatePromise.resolve(result); 29 | unityEditorStatePromise = null; 30 | } 31 | } 32 | 33 | export class GetEditorStateTool implements Tool { 34 | getDefinition(): ToolDefinition { 35 | return { 36 | name: "get_editor_state", 37 | description: 38 | "Retrieve the current state of the Unity Editor, including active GameObjects, selection state, play mode status, scene hierarchy, project structure, and assets. This tool provides a comprehensive snapshot of the editor's current context.", 39 | category: "Editor State", 40 | tags: ["unity", "editor", "state", "hierarchy", "project"], 41 | inputSchema: { 42 | type: "object", 43 | properties: { 44 | format: { 45 | type: "string", 46 | enum: ["Raw"], 47 | description: 48 | "Specify the output format:\n- Raw: Complete editor state including all available data", 49 | default: "Raw", 50 | }, 51 | }, 52 | additionalProperties: false, 53 | }, 54 | returns: { 55 | type: "object", 56 | description: 57 | "Returns a JSON object containing the requested editor state information", 58 | format: 59 | "The response format varies based on the format parameter:\n- Raw: Full UnityEditorState object", 60 | }, 61 | examples: [ 62 | { 63 | description: "Get complete editor state", 64 | input: { format: "Raw" }, 65 | output: 66 | '{ "activeGameObjects": ["Main Camera", "Directional Light"], ... }', 67 | }, 68 | ], 69 | }; 70 | } 71 | 72 | async execute(args: any, context: ToolContext) { 73 | const validFormats = ["Raw"]; 74 | const format = (args?.format as string) || "Raw"; 75 | 76 | if (args?.format && !validFormats.includes(format)) { 77 | throw new McpError( 78 | ErrorCode.InvalidParams, 79 | `Invalid format: "${format}". Valid formats are: ${validFormats.join( 80 | ", ", 81 | )}`, 82 | ); 83 | } 84 | 85 | try { 86 | // Clear previous logs and set command start time 87 | const startLogIndex = context.logBuffer.length; 88 | unityEditorStateTime = Date.now(); 89 | 90 | // Send command to Unity to get editor state 91 | context.unityConnection!.sendMessage("getEditorState", {}); 92 | 93 | // Wait for result with timeout handling 94 | const timeoutMs = 60_000; 95 | const editorState = await Promise.race([ 96 | new Promise((resolve, reject) => { 97 | unityEditorStatePromise = { resolve, reject }; 98 | }), 99 | new Promise((_, reject) => 100 | setTimeout( 101 | () => 102 | reject( 103 | new Error( 104 | `Getting editor state timed out after ${ 105 | timeoutMs / 1000 106 | } seconds. This may indicate an issue with the Unity Editor.`, 107 | ), 108 | ), 109 | timeoutMs, 110 | ), 111 | ), 112 | ]); 113 | 114 | // Process the response based on format 115 | let responseData: any; 116 | switch (format) { 117 | case "Raw": 118 | responseData = editorState; 119 | break; 120 | } 121 | 122 | return { 123 | content: [ 124 | { 125 | type: "text", 126 | text: JSON.stringify(responseData, null, 2), 127 | }, 128 | ], 129 | }; 130 | } catch (error) { 131 | // Enhanced error handling 132 | if (error instanceof Error && error.message.includes("timed out")) { 133 | throw new McpError(ErrorCode.InternalError, error.message); 134 | } 135 | 136 | throw new McpError( 137 | ErrorCode.InternalError, 138 | `Failed to process editor state: ${ 139 | error instanceof Error ? error.message : "Unknown error" 140 | }`, 141 | ); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /unity-mcp-server/src/tools/GetLogsTool.ts: -------------------------------------------------------------------------------- 1 | import { LogEntry, Tool, ToolContext, ToolDefinition } from "./types.js"; 2 | 3 | export class GetLogsTool implements Tool { 4 | getDefinition(): ToolDefinition { 5 | return { 6 | name: "get_logs", 7 | description: 8 | "Retrieve and filter Unity Editor logs with comprehensive filtering options. This tool provides access to editor logs, console messages, warnings, errors, and exceptions with powerful filtering capabilities.", 9 | category: "Debugging", 10 | tags: ["unity", "editor", "logs", "debugging", "console"], 11 | inputSchema: { 12 | type: "object", 13 | properties: { 14 | types: { 15 | type: "array", 16 | items: { 17 | type: "string", 18 | enum: ["Log", "Warning", "Error", "Exception"], 19 | description: "Log entry types to include", 20 | }, 21 | description: 22 | "Filter logs by type. If not specified, all types are included.", 23 | examples: [ 24 | ["Error", "Exception"], 25 | ["Log", "Warning"], 26 | ], 27 | }, 28 | count: { 29 | type: "number", 30 | description: "Maximum number of log entries to return", 31 | minimum: 1, 32 | maximum: 1000, 33 | default: 100, 34 | }, 35 | fields: { 36 | type: "array", 37 | items: { 38 | type: "string", 39 | enum: ["message", "stackTrace", "logType", "timestamp"], 40 | }, 41 | description: 42 | "Specify which fields to include in the output. If not specified, all fields are included.", 43 | examples: [ 44 | ["message", "logType"], 45 | ["message", "stackTrace", "timestamp"], 46 | ], 47 | }, 48 | messageContains: { 49 | type: "string", 50 | description: 51 | "Filter logs to only include entries where the message contains this string (case-sensitive)", 52 | minLength: 1, 53 | }, 54 | stackTraceContains: { 55 | type: "string", 56 | description: 57 | "Filter logs to only include entries where the stack trace contains this string (case-sensitive)", 58 | minLength: 1, 59 | }, 60 | timestampAfter: { 61 | type: "string", 62 | description: "Filter logs after this ISO timestamp (inclusive)", 63 | format: "date-time", 64 | example: "2024-01-14T00:00:00Z", 65 | }, 66 | timestampBefore: { 67 | type: "string", 68 | description: "Filter logs before this ISO timestamp (inclusive)", 69 | format: "date-time", 70 | example: "2024-01-14T23:59:59Z", 71 | }, 72 | }, 73 | additionalProperties: false, 74 | }, 75 | returns: { 76 | type: "array", 77 | description: 78 | "Returns an array of log entries matching the specified filters", 79 | format: "Array of objects containing requested log entry fields", 80 | }, 81 | examples: [ 82 | { 83 | description: "Get recent error logs", 84 | input: { 85 | types: ["Error", "Exception"], 86 | count: 10, 87 | fields: ["message", "timestamp"], 88 | }, 89 | output: 90 | '[{"message": "NullReferenceException", "timestamp": "2024-01-14T12:00:00Z"}, ...]', 91 | }, 92 | { 93 | description: "Search logs for specific message", 94 | input: { 95 | messageContains: "Player", 96 | fields: ["message", "logType"], 97 | }, 98 | output: 99 | '[{"message": "Player position updated", "logType": "Log"}, ...]', 100 | }, 101 | ], 102 | }; 103 | } 104 | 105 | async execute(args: any, context: ToolContext) { 106 | const options = { 107 | types: args?.types as string[] | undefined, 108 | count: (args?.count as number) || 100, 109 | fields: args?.fields as string[] | undefined, 110 | messageContains: args?.messageContains as string | undefined, 111 | stackTraceContains: args?.stackTraceContains as string | undefined, 112 | timestampAfter: args?.timestampAfter as string | undefined, 113 | timestampBefore: args?.timestampBefore as string | undefined, 114 | }; 115 | 116 | const logs = this.filterLogs(context.logBuffer, options); 117 | return { 118 | content: [ 119 | { 120 | type: "text", 121 | text: JSON.stringify(logs, null, 2), 122 | }, 123 | ], 124 | }; 125 | } 126 | 127 | private filterLogs( 128 | logBuffer: LogEntry[], 129 | options: { 130 | types?: string[]; 131 | count?: number; 132 | fields?: string[]; 133 | messageContains?: string; 134 | stackTraceContains?: string; 135 | timestampAfter?: string; 136 | timestampBefore?: string; 137 | }, 138 | ): any[] { 139 | const { 140 | types, 141 | count = 100, 142 | fields, 143 | messageContains, 144 | stackTraceContains, 145 | timestampAfter, 146 | timestampBefore, 147 | } = options; 148 | 149 | // First apply all filters 150 | let filteredLogs = logBuffer.filter((log) => { 151 | // Type filter 152 | if (types && !types.includes(log.logType)) return false; 153 | 154 | // Message content filter 155 | if (messageContains && !log.message.includes(messageContains)) 156 | return false; 157 | 158 | // Stack trace content filter 159 | if (stackTraceContains && !log.stackTrace.includes(stackTraceContains)) 160 | return false; 161 | 162 | // Timestamp filters 163 | if (timestampAfter && new Date(log.timestamp) < new Date(timestampAfter)) 164 | return false; 165 | if ( 166 | timestampBefore && 167 | new Date(log.timestamp) > new Date(timestampBefore) 168 | ) 169 | return false; 170 | 171 | return true; 172 | }); 173 | 174 | // Then apply count limit 175 | filteredLogs = filteredLogs.slice(-count); 176 | 177 | // Finally apply field selection if specified 178 | if (fields?.length) { 179 | return filteredLogs.map((log) => { 180 | const selectedFields: Partial = {}; 181 | fields.forEach((field) => { 182 | if ( 183 | field in log && 184 | (field === "message" || 185 | field === "stackTrace" || 186 | field === "logType" || 187 | field === "timestamp") 188 | ) { 189 | selectedFields[field as keyof LogEntry] = 190 | log[field as keyof LogEntry]; 191 | } 192 | }); 193 | return selectedFields; 194 | }); 195 | } 196 | 197 | return filteredLogs; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /unity-mcp-server/src/tools/index.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteEditorCommandTool } from "./ExecuteEditorCommandTool.js"; 2 | import { GetEditorStateTool } from "./GetEditorStateTool.js"; 3 | import { GetLogsTool } from "./GetLogsTool.js"; 4 | import { Tool } from "./types.js"; 5 | 6 | export * from "./types.js"; 7 | 8 | export function getAllTools(): Tool[] { 9 | return [ 10 | new GetEditorStateTool(), 11 | new ExecuteEditorCommandTool(), 12 | new GetLogsTool(), 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /unity-mcp-server/src/tools/types.ts: -------------------------------------------------------------------------------- 1 | import { UnityConnection } from "../communication/UnityConnection.js"; 2 | 3 | export interface LogEntry { 4 | message: string; 5 | stackTrace: string; 6 | logType: string; 7 | timestamp: string; 8 | } 9 | 10 | export interface ToolDefinition { 11 | name: string; 12 | description: string; 13 | category: string; 14 | tags: string[]; 15 | inputSchema: object; 16 | returns: object; 17 | examples: { 18 | description: string; 19 | input: any; 20 | output: string; 21 | }[]; 22 | errorHandling?: { 23 | description: string; 24 | scenarios: { 25 | error: string; 26 | handling: string; 27 | }[]; 28 | }; 29 | } 30 | 31 | export interface ToolContext { 32 | unityConnection: UnityConnection; 33 | logBuffer: LogEntry[]; 34 | } 35 | 36 | export interface Tool { 37 | getDefinition(): ToolDefinition; 38 | execute(args: any, context: ToolContext): Promise; 39 | } 40 | -------------------------------------------------------------------------------- /unity-mcp-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | --------------------------------------------------------------------------------