├── .gitignore ├── CHANGELOG.md ├── CHANGELOG.md.meta ├── CODE_OF_CONDUCT.md ├── CODE_OF_CONDUCT.md.meta ├── Editor.meta ├── Editor ├── GamePilot.UnityMCP.asmdef ├── GamePilot.UnityMCP.asmdef.meta ├── MCPCodeExecutor.cs ├── MCPCodeExecutor.cs.meta ├── MCPConnectionManager.cs ├── MCPConnectionManager.cs.meta ├── MCPDataCollector.cs ├── MCPDataCollector.cs.meta ├── MCPLogger.cs ├── MCPLogger.cs.meta ├── MCPManager.cs ├── MCPManager.cs.meta ├── MCPMessageHandler.cs ├── MCPMessageHandler.cs.meta ├── MCPMessageSender.cs ├── MCPMessageSender.cs.meta ├── Models.meta ├── Models │ ├── MCPEditorState.cs │ ├── MCPEditorState.cs.meta │ ├── MCPSceneInfo.cs │ └── MCPSceneInfo.cs.meta ├── UI.meta └── UI │ ├── MCPDebugSettings.json │ ├── MCPDebugSettings.json.meta │ ├── MCPDebugWindow.cs │ ├── MCPDebugWindow.cs.meta │ ├── MCPDebugWindow.uss │ ├── MCPDebugWindow.uss.meta │ ├── MCPDebugWindow.uxml │ └── MCPDebugWindow.uxml.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── mcpInspector.png ├── mcpInspector.png.meta ├── mcpServer.meta ├── mcpServer ├── .dockerignore ├── .env.example ├── Dockerfile ├── Dockerfile.meta ├── MCPSummary.md ├── MCPSummary.md.meta ├── build.meta ├── build │ ├── filesystemTools.js │ ├── filesystemTools.js.meta │ ├── index.js │ ├── index.js.meta │ ├── toolDefinitions.js │ ├── toolDefinitions.js.meta │ ├── types.js │ ├── types.js.meta │ ├── websocketHandler.js │ └── websocketHandler.js.meta ├── docker-compose.yml ├── docker-compose.yml.meta ├── node_modules.meta ├── package-lock.json ├── package-lock.json.meta ├── package.json ├── package.json.meta ├── smithery.yaml ├── src.meta ├── src │ ├── filesystemTools.ts │ ├── filesystemTools.ts.meta │ ├── index.ts │ ├── index.ts.meta │ ├── toolDefinitions.ts │ ├── toolDefinitions.ts.meta │ ├── types.ts │ ├── types.ts.meta │ ├── websocketHandler.ts │ └── websocketHandler.ts.meta ├── tsconfig.json └── tsconfig.json.meta ├── package.json └── package.json.meta /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | 138 | 139 | 140 | 141 | 142 | 143 | #unity stuff 144 | # This .gitignore file should be placed at the root of your Unity project directory 145 | # 146 | # Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore 147 | # 148 | .utmp/ 149 | /[Ll]ibrary/ 150 | /[Tt]emp/ 151 | /[Oo]bj/ 152 | /[Bb]uild/ 153 | /[Bb]uilds/ 154 | /[Ll]ogs/ 155 | /[Uu]ser[Ss]ettings/ 156 | 157 | # MemoryCaptures can get excessive in size. 158 | # They also could contain extremely sensitive data 159 | /[Mm]emoryCaptures/ 160 | 161 | # Recordings can get excessive in size 162 | /[Rr]ecordings/ 163 | 164 | # Uncomment this line if you wish to ignore the asset store tools plugin 165 | # /[Aa]ssets/AssetStoreTools* 166 | 167 | # Autogenerated Jetbrains Rider plugin 168 | /[Aa]ssets/Plugins/Editor/JetBrains* 169 | 170 | # Visual Studio cache directory 171 | .vs/ 172 | 173 | # Gradle cache directory 174 | .gradle/ 175 | 176 | # Autogenerated VS/MD/Consulo solution and project files 177 | ExportedObj/ 178 | .consulo/ 179 | *.csproj 180 | *.unityproj 181 | *.sln 182 | *.suo 183 | *.tmp 184 | *.user 185 | *.userprefs 186 | *.pidb 187 | *.booproj 188 | *.svd 189 | *.pdb 190 | *.mdb 191 | *.opendb 192 | *.VC.db 193 | 194 | # Unity3D generated meta files 195 | *.pidb.meta 196 | *.pdb.meta 197 | *.mdb.meta 198 | 199 | # Unity3D generated file on crash reports 200 | sysinfo.txt 201 | 202 | # Builds 203 | *.apk 204 | *.aab 205 | *.unitypackage 206 | *.unitypackage.meta 207 | *.app 208 | 209 | # Crashlytics generated file 210 | crashlytics-build.properties 211 | 212 | # Packed Addressables 213 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* 214 | 215 | # Temporary auto-generated Android Assets 216 | /[Aa]ssets/[Ss]treamingAssets/aa.meta 217 | /[Aa]ssets/[Ss]treamingAssets/aa/* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.1] - 2023-11-11 2 | 3 | ### First Release 4 | - Initial release of Unity MCP Integration 5 | - WebSocket-based communication between Unity Editor and MCP server 6 | - MCP tools implementation: 7 | - `get_editor_state` for retrieving Unity project information 8 | - `get_current_scene_info` for scene hierarchy details 9 | - `get_game_objects_info` for specific GameObject information 10 | - `execute_editor_command` for executing C# code in Unity Editor 11 | - `get_logs` for accessing Unity console logs 12 | - Debug window accessible from Window > MCP Debug menu 13 | - Connection status monitoring 14 | - Support for Unity 2021.3+ 15 | - Node.js MCP server (TypeScript implementation) 16 | - Comprehensive documentation 17 | - MIT License -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 449feb35e0a9c0f4892bddbd92b7cda9 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Contributor Covenant Code of Conduct 2 | Our Pledge 3 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 4 | 5 | Our Standards 6 | Examples of behavior that contributes to creating a positive environment include: 7 | 8 | Using welcoming and inclusive language 9 | Being respectful of differing viewpoints and experiences 10 | Gracefully accepting constructive criticism 11 | Focusing on what is best for the community 12 | Showing empathy towards other community members 13 | Examples of unacceptable behavior by participants include: 14 | 15 | The use of sexualized language or imagery and unwelcome sexual attention or advances 16 | Trolling, insulting/derogatory comments, and personal or political attacks 17 | Public or private harassment 18 | Publishing others' private information, such as a physical or electronic address, without explicit permission 19 | Other conduct which could reasonably be considered inappropriate in a professional setting 20 | Our Responsibilities 21 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 24 | 25 | Scope 26 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 27 | 28 | Enforcement 29 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at {{ email }}. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 30 | 31 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 32 | 33 | Attribution 34 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 084b658a71423b240873ab338b108d62 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 20b961508662b454d9f4847790575d60 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/GamePilot.UnityMCP.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GamePilot.UnityMCP", 3 | "rootNamespace": "Plugins.GamePilot.Editor.MCP", 4 | "includePlatforms": [ 5 | "Editor" 6 | ], 7 | "excludePlatforms": [], 8 | "allowUnsafeCode": false, 9 | "overrideReferences": false, 10 | "precompiledReferences": [ 11 | "Newtonsoft.Json.dll" 12 | ], 13 | "autoReferenced": true, 14 | "defineConstraints": [], 15 | "versionDefines": [], 16 | "noEngineReferences": false 17 | } -------------------------------------------------------------------------------- /Editor/GamePilot.UnityMCP.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 69e230eb498aed749915f0a8fa83f68a 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/MCPCodeExecutor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.CodeDom.Compiler; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Microsoft.CSharp; 6 | using UnityEngine; 7 | using UnityEditor; 8 | 9 | namespace Plugins.GamePilot.Editor.MCP 10 | { 11 | public class MCPCodeExecutor 12 | { 13 | private readonly List logs = new List(); 14 | private readonly List errors = new List(); 15 | private readonly List warnings = new List(); 16 | 17 | public object ExecuteCode(string code) 18 | { 19 | logs.Clear(); 20 | errors.Clear(); 21 | warnings.Clear(); 22 | 23 | // Add log handler to capture output during execution 24 | Application.logMessageReceived += LogHandler; 25 | 26 | try 27 | { 28 | return ExecuteCommand(code); 29 | } 30 | catch (Exception ex) 31 | { 32 | string errorMessage = $"Code execution failed: {ex.Message}\n{ex.StackTrace}"; 33 | Debug.LogError(errorMessage); 34 | errors.Add(errorMessage); 35 | return null; 36 | } 37 | finally 38 | { 39 | Application.logMessageReceived -= LogHandler; 40 | GC.Collect(); 41 | GC.WaitForPendingFinalizers(); 42 | } 43 | } 44 | 45 | private object ExecuteCommand(string code) 46 | { 47 | // The code should define a class called "McpScript" with a static method "Execute" 48 | // Less restrictive on what the code can contain (namespaces, classes, etc.) 49 | using (var provider = new CSharpCodeProvider()) 50 | { 51 | var options = new CompilerParameters 52 | { 53 | GenerateInMemory = true, 54 | IncludeDebugInformation = true 55 | }; 56 | 57 | // Add essential references 58 | AddEssentialReferences(options); 59 | 60 | // Compile the code as provided - with no wrapping 61 | var results = provider.CompileAssemblyFromSource(options, code); 62 | 63 | if (results.Errors.HasErrors) 64 | { 65 | var errorMessages = new List(); 66 | foreach (CompilerError error in results.Errors) 67 | { 68 | errorMessages.Add($"Line {error.Line}: {error.ErrorText}"); 69 | } 70 | throw new Exception("Compilation failed: " + string.Join("\n", errorMessages)); 71 | } 72 | 73 | // Get the compiled assembly and execute the code via the McpScript.Execute method 74 | var assembly = results.CompiledAssembly; 75 | var type = assembly.GetType("McpScript"); 76 | if (type == null) 77 | { 78 | throw new Exception("Could not find McpScript class in compiled assembly. Make sure your code defines a public class named 'McpScript'."); 79 | } 80 | 81 | var method = type.GetMethod("Execute"); 82 | if (method == null) 83 | { 84 | throw new Exception("Could not find Execute method in McpScript class. Make sure your code includes a public static method named 'Execute'."); 85 | } 86 | 87 | return method.Invoke(null, null); 88 | } 89 | } 90 | 91 | private void AddEssentialReferences(CompilerParameters options) 92 | { 93 | // Only add the most essential references to avoid conflicts 94 | try 95 | { 96 | // Core Unity and .NET references 97 | options.ReferencedAssemblies.Add(typeof(UnityEngine.Object).Assembly.Location); // UnityEngine 98 | options.ReferencedAssemblies.Add(typeof(UnityEditor.Editor).Assembly.Location); // UnityEditor 99 | options.ReferencedAssemblies.Add(typeof(System.Object).Assembly.Location); // mscorlib 100 | 101 | // Add System.Core for LINQ 102 | var systemCore = AppDomain.CurrentDomain.GetAssemblies() 103 | .FirstOrDefault(a => a.GetName().Name == "System.Core"); 104 | if (systemCore != null && !string.IsNullOrEmpty(systemCore.Location)) 105 | { 106 | options.ReferencedAssemblies.Add(systemCore.Location); 107 | } 108 | 109 | // Add netstandard reference 110 | var netStandardAssembly = AppDomain.CurrentDomain.GetAssemblies() 111 | .FirstOrDefault(a => a.GetName().Name == "netstandard"); 112 | if (netStandardAssembly != null && !string.IsNullOrEmpty(netStandardAssembly.Location)) 113 | { 114 | options.ReferencedAssemblies.Add(netStandardAssembly.Location); 115 | } 116 | 117 | // Add essential Unity modules 118 | AddUnityModule(options, "UnityEngine.CoreModule"); 119 | AddUnityModule(options, "UnityEngine.PhysicsModule"); 120 | AddUnityModule(options, "UnityEngine.UIModule"); 121 | AddUnityModule(options, "UnityEngine.InputModule"); 122 | AddUnityModule(options, "UnityEngine.AnimationModule"); 123 | AddUnityModule(options, "UnityEngine.IMGUIModule"); 124 | } 125 | catch (Exception ex) 126 | { 127 | Debug.LogWarning($"Error adding assembly references: {ex.Message}"); 128 | } 129 | } 130 | 131 | private void AddUnityModule(CompilerParameters options, string moduleName) 132 | { 133 | try 134 | { 135 | var assembly = AppDomain.CurrentDomain.GetAssemblies() 136 | .FirstOrDefault(a => a.GetName().Name == moduleName); 137 | 138 | if (assembly != null && !string.IsNullOrEmpty(assembly.Location) && 139 | !options.ReferencedAssemblies.Contains(assembly.Location)) 140 | { 141 | options.ReferencedAssemblies.Add(assembly.Location); 142 | } 143 | } 144 | catch (Exception ex) 145 | { 146 | Debug.LogWarning($"Failed to add Unity module {moduleName}: {ex.Message}"); 147 | } 148 | } 149 | 150 | private void LogHandler(string message, string stackTrace, LogType type) 151 | { 152 | switch (type) 153 | { 154 | case LogType.Log: 155 | logs.Add(message); 156 | break; 157 | case LogType.Warning: 158 | warnings.Add(message); 159 | break; 160 | case LogType.Error: 161 | case LogType.Exception: 162 | case LogType.Assert: 163 | errors.Add($"{message}\n{stackTrace}"); 164 | break; 165 | } 166 | } 167 | 168 | public string[] GetLogs() => logs.ToArray(); 169 | public string[] GetErrors() => errors.ToArray(); 170 | public string[] GetWarnings() => warnings.ToArray(); 171 | } 172 | } -------------------------------------------------------------------------------- /Editor/MCPCodeExecutor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c4731513c220b7e439282d9b939a0556 -------------------------------------------------------------------------------- /Editor/MCPConnectionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using UnityEngine; 7 | 8 | namespace Plugins.GamePilot.Editor.MCP 9 | { 10 | public class MCPConnectionManager 11 | { 12 | private static readonly string ComponentName = "MCPConnectionManager"; 13 | private ClientWebSocket webSocket; 14 | private Uri serverUri = new Uri("ws://localhost:5010"); // Changed to allow changing 15 | private readonly CancellationTokenSource cts = new CancellationTokenSource(); 16 | private bool isConnected = false; 17 | private float reconnectTimer = 0f; 18 | private readonly float reconnectInterval = 5f; 19 | private string lastErrorMessage = string.Empty; 20 | 21 | // Statistics 22 | private int messagesSent = 0; 23 | private int messagesReceived = 0; 24 | private int reconnectAttempts = 0; 25 | 26 | // Events 27 | public event Action OnMessageReceived; 28 | public event Action OnConnected; 29 | public event Action OnDisconnected; 30 | public event Action OnError; 31 | 32 | // Properly track the connection state using the WebSocket state and our own flag 33 | public bool IsConnected => isConnected && webSocket?.State == WebSocketState.Open; 34 | public string LastErrorMessage => lastErrorMessage; 35 | public int MessagesSent => messagesSent; 36 | public int MessagesReceived => messagesReceived; 37 | public int ReconnectAttempts => reconnectAttempts; 38 | public Uri ServerUri 39 | { 40 | get => serverUri; 41 | set => serverUri = value; 42 | } 43 | 44 | public MCPConnectionManager() 45 | { 46 | MCPLogger.InitializeComponent(ComponentName); 47 | webSocket = new ClientWebSocket(); 48 | webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(30); 49 | } 50 | 51 | public async void Connect() 52 | { 53 | // Double check connections that look open but may be stale 54 | if (webSocket != null && webSocket.State == WebSocketState.Open) 55 | { 56 | try 57 | { 58 | // Try to send a ping to verify connection is truly active 59 | bool connectionIsActive = await TestConnection(); 60 | if (connectionIsActive) 61 | { 62 | MCPLogger.Log(ComponentName, "WebSocket already connected and active"); 63 | return; 64 | } 65 | else 66 | { 67 | MCPLogger.Log(ComponentName, "WebSocket appears open but is stale, reconnecting..."); 68 | // Fall through to reconnection logic 69 | } 70 | } 71 | catch (Exception) 72 | { 73 | MCPLogger.Log(ComponentName, "WebSocket appears open but failed ping test, reconnecting..."); 74 | // Fall through to reconnection logic 75 | } 76 | } 77 | else if (webSocket != null && webSocket.State == WebSocketState.Connecting) 78 | { 79 | MCPLogger.Log(ComponentName, "WebSocket is already connecting"); 80 | return; 81 | } 82 | 83 | // Clean up any existing socket 84 | if (webSocket != null) 85 | { 86 | try 87 | { 88 | if (webSocket.State == WebSocketState.Open) 89 | { 90 | await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, 91 | "Reconnecting", CancellationToken.None); 92 | } 93 | webSocket.Dispose(); 94 | } 95 | catch (Exception ex) 96 | { 97 | MCPLogger.LogWarning(ComponentName, $"Error cleaning up WebSocket: {ex.Message}"); 98 | } 99 | } 100 | 101 | webSocket = new ClientWebSocket(); 102 | webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(30); 103 | 104 | try 105 | { 106 | MCPLogger.Log(ComponentName, $"Connecting to MCP Server at {serverUri}"); 107 | 108 | var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); 109 | using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, timeout.Token); 110 | 111 | await webSocket.ConnectAsync(serverUri, linkedCts.Token); 112 | isConnected = true; 113 | 114 | MCPLogger.Log(ComponentName, "Successfully connected to MCP Server"); 115 | OnConnected?.Invoke(); 116 | StartReceiving(); 117 | } 118 | catch (OperationCanceledException) 119 | { 120 | lastErrorMessage = "Connection attempt timed out"; 121 | MCPLogger.LogError(ComponentName, lastErrorMessage); 122 | OnError?.Invoke(lastErrorMessage); 123 | isConnected = false; 124 | OnDisconnected?.Invoke(); 125 | } 126 | catch (WebSocketException we) 127 | { 128 | lastErrorMessage = $"WebSocket error: {we.Message}"; 129 | MCPLogger.LogError(ComponentName, lastErrorMessage); 130 | OnError?.Invoke(lastErrorMessage); 131 | isConnected = false; 132 | OnDisconnected?.Invoke(); 133 | } 134 | catch (Exception e) 135 | { 136 | lastErrorMessage = $"Failed to connect: {e.Message}"; 137 | MCPLogger.LogError(ComponentName, lastErrorMessage); 138 | OnError?.Invoke(lastErrorMessage); 139 | isConnected = false; 140 | OnDisconnected?.Invoke(); 141 | } 142 | } 143 | 144 | // Test if connection is still valid with a simple ping 145 | private async Task TestConnection() 146 | { 147 | try 148 | { 149 | // Simple ping test - send a 1-byte message 150 | byte[] pingData = new byte[1] { 0 }; 151 | await webSocket.SendAsync( 152 | new ArraySegment(pingData), 153 | WebSocketMessageType.Binary, 154 | true, 155 | CancellationToken.None); 156 | 157 | return true; 158 | } 159 | catch 160 | { 161 | return false; 162 | } 163 | } 164 | 165 | public void Reconnect() 166 | { 167 | reconnectAttempts++; 168 | MCPLogger.Log(ComponentName, "Manually reconnecting..."); 169 | reconnectTimer = 0; 170 | Connect(); 171 | } 172 | 173 | public void CheckConnection() 174 | { 175 | if (!isConnected || webSocket?.State != WebSocketState.Open) 176 | { 177 | reconnectTimer += UnityEngine.Time.deltaTime; 178 | 179 | if (reconnectTimer >= reconnectInterval) 180 | { 181 | reconnectAttempts++; 182 | MCPLogger.Log(ComponentName, "Attempting reconnection..."); 183 | reconnectTimer = 0f; 184 | Connect(); 185 | } 186 | } 187 | } 188 | 189 | private async void StartReceiving() 190 | { 191 | var buffer = new byte[8192]; // 8KB buffer 192 | 193 | try 194 | { 195 | while (webSocket.State == WebSocketState.Open && !cts.IsCancellationRequested) 196 | { 197 | var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cts.Token); 198 | 199 | if (result.MessageType == WebSocketMessageType.Text) 200 | { 201 | messagesReceived++; 202 | var message = Encoding.UTF8.GetString(buffer, 0, result.Count); 203 | MCPLogger.Log(ComponentName, $"Received message: {message.Substring(0, Math.Min(100, message.Length))}..."); 204 | OnMessageReceived?.Invoke(message); 205 | } 206 | else if (result.MessageType == WebSocketMessageType.Close) 207 | { 208 | MCPLogger.Log(ComponentName, "Server requested connection close"); 209 | isConnected = false; 210 | OnDisconnected?.Invoke(); 211 | break; 212 | } 213 | } 214 | } 215 | catch (OperationCanceledException) 216 | { 217 | // Normal cancellation, don't log as error 218 | MCPLogger.Log(ComponentName, "WebSocket receiving was canceled"); 219 | } 220 | catch (WebSocketException wsEx) 221 | { 222 | if (!cts.IsCancellationRequested) 223 | { 224 | lastErrorMessage = $"WebSocket error: {wsEx.Message}"; 225 | MCPLogger.LogError(ComponentName, lastErrorMessage); 226 | OnError?.Invoke(lastErrorMessage); 227 | } 228 | } 229 | catch (Exception e) 230 | { 231 | if (!cts.IsCancellationRequested) 232 | { 233 | lastErrorMessage = $"Connection error: {e.Message}"; 234 | MCPLogger.LogError(ComponentName, lastErrorMessage); 235 | OnError?.Invoke(lastErrorMessage); 236 | } 237 | } 238 | finally 239 | { 240 | if (isConnected) 241 | { 242 | isConnected = false; 243 | OnDisconnected?.Invoke(); 244 | } 245 | } 246 | } 247 | 248 | public async Task SendMessageAsync(string message) 249 | { 250 | if (!isConnected || webSocket?.State != WebSocketState.Open) 251 | { 252 | MCPLogger.LogWarning(ComponentName, "Cannot send message: not connected"); 253 | return; 254 | } 255 | 256 | try 257 | { 258 | var buffer = Encoding.UTF8.GetBytes(message); 259 | await webSocket.SendAsync( 260 | new ArraySegment(buffer), 261 | WebSocketMessageType.Text, 262 | true, 263 | cts.Token); 264 | 265 | messagesSent++; 266 | MCPLogger.Log(ComponentName, $"Sent message: {message.Substring(0, Math.Min(100, message.Length))}..."); 267 | } 268 | catch (OperationCanceledException) 269 | { 270 | // Normal cancellation, don't log as error 271 | MCPLogger.Log(ComponentName, "Send operation was canceled"); 272 | } 273 | catch (WebSocketException wsEx) 274 | { 275 | lastErrorMessage = $"WebSocket send error: {wsEx.Message}"; 276 | MCPLogger.LogError(ComponentName, lastErrorMessage); 277 | OnError?.Invoke(lastErrorMessage); 278 | 279 | // Connection might be broken, mark as disconnected 280 | isConnected = false; 281 | OnDisconnected?.Invoke(); 282 | } 283 | catch (Exception e) 284 | { 285 | lastErrorMessage = $"Failed to send message: {e.Message}"; 286 | MCPLogger.LogError(ComponentName, lastErrorMessage); 287 | OnError?.Invoke(lastErrorMessage); 288 | 289 | // Connection might be broken, mark as disconnected 290 | isConnected = false; 291 | OnDisconnected?.Invoke(); 292 | } 293 | } 294 | 295 | public void Disconnect() 296 | { 297 | try 298 | { 299 | MCPLogger.Log(ComponentName, "Disconnecting from server"); 300 | 301 | // Cancel any pending operations 302 | if (!cts.IsCancellationRequested) 303 | { 304 | cts.Cancel(); 305 | } 306 | 307 | if (webSocket != null && webSocket.State == WebSocketState.Open) 308 | { 309 | // Begin graceful close 310 | var closeTask = webSocket.CloseAsync( 311 | WebSocketCloseStatus.NormalClosure, 312 | "Client disconnecting", 313 | CancellationToken.None); 314 | 315 | // Give it a moment to close gracefully 316 | Task.WaitAny(new[] { closeTask }, 1000); 317 | } 318 | 319 | // Dispose resources 320 | if (webSocket != null) 321 | { 322 | webSocket.Dispose(); 323 | webSocket = null; 324 | } 325 | } 326 | catch (Exception e) 327 | { 328 | MCPLogger.LogWarning(ComponentName, $"Error during disconnect: {e.Message}"); 329 | } 330 | finally 331 | { 332 | isConnected = false; 333 | OnDisconnected?.Invoke(); 334 | } 335 | } 336 | 337 | ~MCPConnectionManager() 338 | { 339 | // Ensure resources are cleaned up 340 | Disconnect(); 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /Editor/MCPConnectionManager.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 59bf2b20ea42fec45ae1d16d539d0c12 -------------------------------------------------------------------------------- /Editor/MCPDataCollector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | 7 | namespace Plugins.GamePilot.Editor.MCP 8 | { 9 | public enum SceneInfoDetail 10 | { 11 | RootObjectsOnly, 12 | FullHierarchy 13 | } 14 | 15 | public enum GameObjectInfoDetail 16 | { 17 | BasicInfo, 18 | IncludeComponents, 19 | IncludeChildren, 20 | IncludeComponentsAndChildren // New option to include both components and children 21 | } 22 | 23 | public class MCPDataCollector : IDisposable 24 | { 25 | private readonly Queue logBuffer = new Queue(); 26 | private readonly int maxLogBufferSize = 1000; 27 | private bool isLoggingEnabled = true; 28 | 29 | public MCPDataCollector() 30 | { 31 | // Start capturing logs 32 | Application.logMessageReceived += HandleLogMessage; 33 | } 34 | 35 | public void Dispose() 36 | { 37 | // Unsubscribe to prevent memory leaks 38 | Application.logMessageReceived -= HandleLogMessage; 39 | } 40 | 41 | private void HandleLogMessage(string message, string stackTrace, LogType type) 42 | { 43 | if (!isLoggingEnabled) return; 44 | 45 | var logEntry = new LogEntry 46 | { 47 | Message = message, 48 | StackTrace = stackTrace, 49 | Type = type, 50 | Timestamp = DateTime.UtcNow 51 | }; 52 | 53 | lock (logBuffer) 54 | { 55 | logBuffer.Enqueue(logEntry); 56 | while (logBuffer.Count > maxLogBufferSize) 57 | { 58 | logBuffer.Dequeue(); 59 | } 60 | } 61 | } 62 | 63 | public bool IsLoggingEnabled 64 | { 65 | get => isLoggingEnabled; 66 | set 67 | { 68 | if (isLoggingEnabled == value) return; 69 | 70 | isLoggingEnabled = value; 71 | if (value) 72 | { 73 | Application.logMessageReceived += HandleLogMessage; 74 | } 75 | else 76 | { 77 | Application.logMessageReceived -= HandleLogMessage; 78 | } 79 | } 80 | } 81 | 82 | public LogEntry[] GetRecentLogs(int count = 50) 83 | { 84 | lock (logBuffer) 85 | { 86 | return logBuffer.Reverse().Take(count).Reverse().ToArray(); 87 | } 88 | } 89 | 90 | public MCPEditorState GetEditorState() 91 | { 92 | var state = new MCPEditorState 93 | { 94 | ActiveGameObjects = GetActiveGameObjects(), 95 | SelectedObjects = GetSelectedObjects(), 96 | PlayModeState = EditorApplication.isPlaying ? "Playing" : "Stopped", 97 | SceneHierarchy = GetSceneHierarchy(), 98 | Timestamp = DateTime.UtcNow 99 | }; 100 | 101 | return state; 102 | } 103 | 104 | private string[] GetActiveGameObjects() 105 | { 106 | try 107 | { 108 | var foundObjects = GameObject.FindObjectsByType(FindObjectsSortMode.None); 109 | return foundObjects.Where(o => o != null).Select(obj => obj.name).ToArray(); 110 | } 111 | catch (Exception ex) 112 | { 113 | Debug.LogError($"[MCP] Error getting active GameObjects: {ex.Message}"); 114 | return new string[0]; 115 | } 116 | } 117 | 118 | private string[] GetSelectedObjects() 119 | { 120 | try 121 | { 122 | return Selection.gameObjects.Where(o => o != null).Select(obj => obj.name).ToArray(); 123 | } 124 | catch (Exception ex) 125 | { 126 | Debug.LogError($"[MCP] Error getting selected objects: {ex.Message}"); 127 | return new string[0]; 128 | } 129 | } 130 | 131 | private List GetSceneHierarchy() 132 | { 133 | var hierarchy = new List(); 134 | 135 | try 136 | { 137 | var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); 138 | if (scene.IsValid()) 139 | { 140 | var rootObjects = scene.GetRootGameObjects(); 141 | foreach (var root in rootObjects.Where(o => o != null)) 142 | { 143 | hierarchy.Add(GetGameObjectHierarchy(root)); 144 | } 145 | } 146 | } 147 | catch (Exception ex) 148 | { 149 | Debug.LogError($"[MCP] Error getting scene hierarchy: {ex.Message}"); 150 | } 151 | 152 | return hierarchy; 153 | } 154 | 155 | private MCPGameObjectInfo GetGameObjectHierarchy(GameObject obj) 156 | { 157 | if (obj == null) return null; 158 | 159 | try 160 | { 161 | var info = new MCPGameObjectInfo 162 | { 163 | Name = obj.name, 164 | Path = GetGameObjectPath(obj), 165 | Components = obj.GetComponents() 166 | .Where(c => c != null) 167 | .Select(c => c.GetType().Name) 168 | .ToArray(), 169 | Children = new List(), 170 | Active = obj.activeSelf, 171 | Layer = obj.layer, 172 | Tag = obj.tag 173 | }; 174 | 175 | var transform = obj.transform; 176 | for (int i = 0; i < transform.childCount; i++) 177 | { 178 | var childTransform = transform.GetChild(i); 179 | if (childTransform != null && childTransform.gameObject != null) 180 | { 181 | var childInfo = GetGameObjectHierarchy(childTransform.gameObject); 182 | if (childInfo != null) 183 | { 184 | info.Children.Add(childInfo); 185 | } 186 | } 187 | } 188 | 189 | return info; 190 | } 191 | catch (Exception ex) 192 | { 193 | Debug.LogWarning($"[MCP] Error processing GameObject {obj.name}: {ex.Message}"); 194 | return new MCPGameObjectInfo { Name = obj.name, Path = GetGameObjectPath(obj) }; 195 | } 196 | } 197 | 198 | private string GetGameObjectPath(GameObject obj) 199 | { 200 | if (obj == null) return string.Empty; 201 | 202 | try 203 | { 204 | string path = obj.name; 205 | Transform parent = obj.transform.parent; 206 | 207 | while (parent != null) 208 | { 209 | path = parent.name + "/" + path; 210 | parent = parent.parent; 211 | } 212 | 213 | return path; 214 | } 215 | catch (Exception) 216 | { 217 | return obj.name; 218 | } 219 | } 220 | 221 | // New method to get current scene information based on requested detail level 222 | public MCPSceneInfo GetCurrentSceneInfo(SceneInfoDetail detailLevel) 223 | { 224 | try 225 | { 226 | var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); 227 | 228 | var sceneInfo = new MCPSceneInfo 229 | { 230 | Name = scene.name, 231 | Path = scene.path, 232 | IsDirty = scene.isDirty, 233 | RootCount = scene.rootCount 234 | }; 235 | 236 | if (scene.IsValid()) 237 | { 238 | var rootObjects = scene.GetRootGameObjects(); 239 | 240 | // Map scene objects based on detail level requested 241 | switch (detailLevel) 242 | { 243 | case SceneInfoDetail.RootObjectsOnly: 244 | sceneInfo.RootObjects = rootObjects 245 | .Where(o => o != null) 246 | .Select(o => new MCPGameObjectReference 247 | { 248 | Name = o.name, 249 | InstanceID = o.GetInstanceID(), 250 | Path = GetGameObjectPath(o), 251 | Active = o.activeSelf, 252 | ChildCount = o.transform.childCount 253 | }) 254 | .ToList(); 255 | break; 256 | 257 | case SceneInfoDetail.FullHierarchy: 258 | sceneInfo.RootObjects = rootObjects 259 | .Where(o => o != null) 260 | .Select(o => GetGameObjectReferenceWithChildren(o)) 261 | .ToList(); 262 | break; 263 | } 264 | } 265 | 266 | return sceneInfo; 267 | } 268 | catch (Exception ex) 269 | { 270 | Debug.LogError($"[MCP] Error getting scene info: {ex.Message}"); 271 | return new MCPSceneInfo { Name = "Error", ErrorMessage = ex.Message }; 272 | } 273 | } 274 | 275 | // Helper method to create a game object reference with children 276 | private MCPGameObjectReference GetGameObjectReferenceWithChildren(GameObject obj) 277 | { 278 | if (obj == null) return null; 279 | 280 | var reference = new MCPGameObjectReference 281 | { 282 | Name = obj.name, 283 | InstanceID = obj.GetInstanceID(), 284 | Path = GetGameObjectPath(obj), 285 | Active = obj.activeSelf, 286 | ChildCount = obj.transform.childCount, 287 | Children = new List() 288 | }; 289 | 290 | // Add all children 291 | var transform = obj.transform; 292 | for (int i = 0; i < transform.childCount; i++) 293 | { 294 | var childTransform = transform.GetChild(i); 295 | if (childTransform != null && childTransform.gameObject != null) 296 | { 297 | var childRef = GetGameObjectReferenceWithChildren(childTransform.gameObject); 298 | if (childRef != null) 299 | { 300 | reference.Children.Add(childRef); 301 | } 302 | } 303 | } 304 | 305 | return reference; 306 | } 307 | 308 | // Get detailed information about specific game objects 309 | public List GetGameObjectsInfo(int[] objectInstanceIDs, GameObjectInfoDetail detailLevel) 310 | { 311 | var results = new List(); 312 | 313 | try 314 | { 315 | foreach (var id in objectInstanceIDs) 316 | { 317 | var obj = EditorUtility.InstanceIDToObject(id) as GameObject; 318 | if (obj != null) 319 | { 320 | results.Add(GetGameObjectDetail(obj, detailLevel)); 321 | } 322 | } 323 | } 324 | catch (Exception ex) 325 | { 326 | Debug.LogError($"[MCP] Error getting game object details: {ex.Message}"); 327 | } 328 | 329 | return results; 330 | } 331 | 332 | // Helper method to get detailed info about a game object 333 | private MCPGameObjectDetail GetGameObjectDetail(GameObject obj, GameObjectInfoDetail detailLevel) 334 | { 335 | var detail = new MCPGameObjectDetail 336 | { 337 | Name = obj.name, 338 | InstanceID = obj.GetInstanceID(), 339 | Path = GetGameObjectPath(obj), 340 | Active = obj.activeSelf, 341 | ActiveInHierarchy = obj.activeInHierarchy, 342 | Tag = obj.tag, 343 | Layer = obj.layer, 344 | LayerName = LayerMask.LayerToName(obj.layer), 345 | IsStatic = obj.isStatic, 346 | Transform = new MCPTransformInfo 347 | { 348 | Position = obj.transform.position, 349 | Rotation = obj.transform.rotation.eulerAngles, 350 | LocalPosition = obj.transform.localPosition, 351 | LocalRotation = obj.transform.localRotation.eulerAngles, 352 | LocalScale = obj.transform.localScale 353 | } 354 | }; 355 | 356 | // Include components if requested 357 | if (detailLevel == GameObjectInfoDetail.IncludeComponents || 358 | detailLevel == GameObjectInfoDetail.IncludeComponentsAndChildren) 359 | { 360 | detail.Components = obj.GetComponents() 361 | .Where(c => c != null) 362 | .Select(c => new MCPComponentInfo 363 | { 364 | Type = c.GetType().Name, 365 | IsEnabled = GetComponentEnabled(c), 366 | InstanceID = c.GetInstanceID() 367 | }) 368 | .ToList(); 369 | } 370 | 371 | // Include children if requested 372 | if (detailLevel == GameObjectInfoDetail.IncludeChildren || 373 | detailLevel == GameObjectInfoDetail.IncludeComponentsAndChildren) 374 | { 375 | detail.Children = new List(); 376 | var transform = obj.transform; 377 | 378 | for (int i = 0; i < transform.childCount; i++) 379 | { 380 | var childTransform = transform.GetChild(i); 381 | if (childTransform != null && childTransform.gameObject != null) 382 | { 383 | // When including both components and children, make sure to include components for children too 384 | GameObjectInfoDetail childDetailLevel = detailLevel == GameObjectInfoDetail.IncludeComponentsAndChildren 385 | ? GameObjectInfoDetail.IncludeComponentsAndChildren 386 | : GameObjectInfoDetail.BasicInfo; 387 | 388 | var childDetail = GetGameObjectDetail( 389 | childTransform.gameObject, 390 | childDetailLevel); 391 | 392 | detail.Children.Add(childDetail); 393 | } 394 | } 395 | } 396 | 397 | return detail; 398 | } 399 | 400 | // Helper for getting component enabled state 401 | private bool GetComponentEnabled(Component component) 402 | { 403 | // Try to check if component is enabled (for components that support it) 404 | try 405 | { 406 | if (component is Behaviour behaviour) 407 | return behaviour.enabled; 408 | 409 | if (component is Renderer renderer) 410 | return renderer.enabled; 411 | 412 | if (component is Collider collider) 413 | return collider.enabled; 414 | } 415 | catch 416 | { 417 | // Ignore any exceptions 418 | } 419 | 420 | // Default to true for components that don't have an enabled property 421 | return true; 422 | } 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /Editor/MCPDataCollector.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 78a42a491d2a9fc44a0af6c2d0a63d61 -------------------------------------------------------------------------------- /Editor/MCPLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace Plugins.GamePilot.Editor.MCP 6 | { 7 | /// 8 | /// Centralized logging system for MCP components that respects component-level logging settings. 9 | /// 10 | public static class MCPLogger 11 | { 12 | // Dictionary to track enabled status for each component 13 | private static readonly Dictionary componentLoggingEnabled = new Dictionary(); 14 | 15 | // Default log state (off by default) 16 | private static bool globalLoggingEnabled = false; 17 | 18 | /// 19 | /// Enable or disable logging globally for all components 20 | /// 21 | public static bool GlobalLoggingEnabled 22 | { 23 | get => globalLoggingEnabled; 24 | set => globalLoggingEnabled = value; 25 | } 26 | 27 | /// 28 | /// Initialize a component for logging 29 | /// 30 | /// Name of the component 31 | /// Whether logging should be enabled by default 32 | public static void InitializeComponent(string componentName, bool enabledByDefault = false) 33 | { 34 | if (!componentLoggingEnabled.ContainsKey(componentName)) 35 | { 36 | componentLoggingEnabled[componentName] = enabledByDefault; 37 | } 38 | } 39 | 40 | /// 41 | /// Set logging state for a specific component 42 | /// 43 | /// Name of the component 44 | /// Whether logging should be enabled 45 | public static void SetComponentLoggingEnabled(string componentName, bool enabled) 46 | { 47 | // Make sure component exists before setting state 48 | if (!componentLoggingEnabled.ContainsKey(componentName)) 49 | { 50 | InitializeComponent(componentName, false); 51 | } 52 | 53 | componentLoggingEnabled[componentName] = enabled; 54 | } 55 | 56 | /// 57 | /// Check if logging is enabled for a component 58 | /// 59 | /// Name of the component 60 | /// True if logging is enabled 61 | public static bool IsLoggingEnabled(string componentName) 62 | { 63 | // If global logging is disabled, nothing gets logged 64 | if (!globalLoggingEnabled) return false; 65 | 66 | // If component isn't registered, assume it's disabled 67 | if (!componentLoggingEnabled.ContainsKey(componentName)) 68 | { 69 | InitializeComponent(componentName, false); 70 | return false; 71 | } 72 | 73 | // Return component-specific setting 74 | return componentLoggingEnabled[componentName]; 75 | } 76 | 77 | /// 78 | /// Log a message if logging is enabled for the component 79 | /// 80 | /// Name of the component 81 | /// Message to log 82 | public static void Log(string componentName, string message) 83 | { 84 | if (IsLoggingEnabled(componentName)) 85 | { 86 | Debug.Log($"[MCP] [{componentName}] {message}"); 87 | } 88 | } 89 | 90 | /// 91 | /// Log a warning if logging is enabled for the component 92 | /// 93 | /// Name of the component 94 | /// Message to log 95 | public static void LogWarning(string componentName, string message) 96 | { 97 | if (IsLoggingEnabled(componentName)) 98 | { 99 | Debug.LogWarning($"[MCP] [{componentName}] {message}"); 100 | } 101 | } 102 | 103 | /// 104 | /// Log an error if logging is enabled for the component 105 | /// 106 | /// Name of the component 107 | /// Message to log 108 | public static void LogError(string componentName, string message) 109 | { 110 | if (IsLoggingEnabled(componentName)) 111 | { 112 | Debug.LogError($"[MCP] [{componentName}] {message}"); 113 | } 114 | } 115 | 116 | /// 117 | /// Log an exception if logging is enabled for the component 118 | /// 119 | /// Name of the component 120 | /// Exception to log 121 | public static void LogException(string componentName, Exception ex) 122 | { 123 | if (IsLoggingEnabled(componentName)) 124 | { 125 | Debug.LogError($"[MCP] [{componentName}] Exception: {ex.Message}\n{ex.StackTrace}"); 126 | } 127 | } 128 | 129 | /// 130 | /// Get all registered components 131 | /// 132 | public static IEnumerable GetRegisteredComponents() 133 | { 134 | return componentLoggingEnabled.Keys; 135 | } 136 | 137 | /// 138 | /// Get logging state for a component 139 | /// 140 | public static bool GetComponentLoggingEnabled(string componentName) 141 | { 142 | if (!componentLoggingEnabled.ContainsKey(componentName)) 143 | return false; 144 | 145 | return componentLoggingEnabled[componentName]; 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Editor/MCPLogger.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5b0ca890b7bff1a46a468595b4436f17 -------------------------------------------------------------------------------- /Editor/MCPManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor; 3 | using UnityEngine; 4 | 5 | namespace Plugins.GamePilot.Editor.MCP 6 | { 7 | [InitializeOnLoad] 8 | public class MCPManager 9 | { 10 | private static readonly string ComponentName = "MCPManager"; 11 | private static MCPConnectionManager connectionManager; 12 | private static MCPMessageHandler messageHandler; 13 | private static MCPDataCollector dataCollector; 14 | private static MCPMessageSender messageSender; 15 | 16 | private static bool isInitialized = false; 17 | public static bool IsInitialized => isInitialized; 18 | private static bool autoReconnect = false; 19 | 20 | // Constructor called on editor startup due to [InitializeOnLoad] 21 | static MCPManager() 22 | { 23 | // Initialize logger for this component 24 | MCPLogger.InitializeComponent(ComponentName); 25 | 26 | EditorApplication.delayCall += Initialize; 27 | } 28 | 29 | public static void Initialize() 30 | { 31 | if (isInitialized) 32 | return; 33 | 34 | MCPLogger.Log(ComponentName, "Initializing Model Context Protocol system..."); 35 | 36 | try 37 | { 38 | // Create components 39 | dataCollector = new MCPDataCollector(); 40 | connectionManager = new MCPConnectionManager(); 41 | messageSender = new MCPMessageSender(connectionManager); 42 | messageHandler = new MCPMessageHandler(dataCollector, messageSender); 43 | 44 | // Hook up events 45 | connectionManager.OnMessageReceived += messageHandler.HandleMessage; 46 | connectionManager.OnConnected += OnConnected; 47 | connectionManager.OnDisconnected += OnDisconnected; 48 | connectionManager.OnError += OnError; 49 | 50 | // Start connection 51 | connectionManager.Connect(); 52 | 53 | // Register update for connection checking only 54 | EditorApplication.update += Update; 55 | 56 | isInitialized = true; 57 | MCPLogger.Log(ComponentName, "Model Context Protocol system initialized successfully"); 58 | } 59 | catch (Exception ex) 60 | { 61 | MCPLogger.LogException(ComponentName, ex); 62 | Debug.LogError($"[MCP] Failed to initialize MCP system: {ex.Message}\n{ex.StackTrace}"); 63 | } 64 | } 65 | 66 | private static void OnConnected() 67 | { 68 | try 69 | { 70 | MCPLogger.Log(ComponentName, "Connected to MCP server"); 71 | } 72 | catch (Exception ex) 73 | { 74 | MCPLogger.LogException(ComponentName, ex); 75 | } 76 | } 77 | 78 | private static void OnDisconnected() 79 | { 80 | try 81 | { 82 | MCPLogger.Log(ComponentName, "Disconnected from MCP server"); 83 | } 84 | catch (Exception ex) 85 | { 86 | MCPLogger.LogException(ComponentName, ex); 87 | } 88 | } 89 | 90 | private static void OnError(string errorMessage) 91 | { 92 | MCPLogger.LogError(ComponentName, $"Connection error: {errorMessage}"); 93 | } 94 | 95 | private static void Update() 96 | { 97 | try 98 | { 99 | // Check if connection manager needs to reconnect 100 | if (autoReconnect) 101 | { 102 | connectionManager?.CheckConnection(); 103 | } 104 | } 105 | catch (Exception ex) 106 | { 107 | MCPLogger.LogException(ComponentName, ex); 108 | } 109 | } 110 | 111 | // Public methods for manual control 112 | public static void RetryConnection() 113 | { 114 | connectionManager?.Reconnect(); 115 | } 116 | 117 | public static void EnableAutoReconnect(bool enable) 118 | { 119 | autoReconnect = enable; 120 | MCPLogger.Log(ComponentName, $"Auto reconnect set to: {enable}"); 121 | } 122 | 123 | public static bool IsConnected => connectionManager?.IsConnected ?? false; 124 | 125 | public static void Shutdown() 126 | { 127 | if (!isInitialized) return; 128 | 129 | try 130 | { 131 | MCPLogger.Log(ComponentName, "Shutting down MCP system"); 132 | 133 | // Unregister update callbacks 134 | EditorApplication.update -= Update; 135 | 136 | // Disconnect 137 | connectionManager?.Disconnect(); 138 | 139 | // Cleanup 140 | dataCollector?.Dispose(); 141 | 142 | isInitialized = false; 143 | MCPLogger.Log(ComponentName, "System shutdown completed"); 144 | } 145 | catch (Exception ex) 146 | { 147 | MCPLogger.LogException(ComponentName, ex); 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Editor/MCPManager.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ddb54e2141be9ae4796da0e3d353ee3d -------------------------------------------------------------------------------- /Editor/MCPMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Linq; 9 | using System.Linq; 10 | 11 | namespace Plugins.GamePilot.Editor.MCP 12 | { 13 | public class MCPMessageHandler 14 | { 15 | private readonly MCPDataCollector dataCollector; 16 | private readonly MCPCodeExecutor codeExecutor; 17 | private readonly MCPMessageSender messageSender; 18 | 19 | public MCPMessageHandler(MCPDataCollector dataCollector, MCPMessageSender messageSender) 20 | { 21 | this.dataCollector = dataCollector ?? throw new ArgumentNullException(nameof(dataCollector)); 22 | this.messageSender = messageSender ?? throw new ArgumentNullException(nameof(messageSender)); 23 | this.codeExecutor = new MCPCodeExecutor(); 24 | } 25 | 26 | public async void HandleMessage(string messageJson) 27 | { 28 | if (string.IsNullOrEmpty(messageJson)) return; 29 | 30 | try 31 | { 32 | Debug.Log($"[MCP] Received message: {messageJson}"); 33 | var message = JsonConvert.DeserializeObject(messageJson); 34 | if (message == null) return; 35 | 36 | switch (message.Type) 37 | { 38 | case "selectGameObject": 39 | await HandleSelectGameObjectAsync(message.Data); 40 | break; 41 | 42 | case "togglePlayMode": 43 | await HandleTogglePlayModeAsync(); 44 | break; 45 | 46 | case "executeEditorCommand": 47 | await HandleExecuteCommandAsync(message.Data); 48 | break; 49 | 50 | case "requestEditorState": // Consolidated to a single message type 51 | await HandleRequestEditorStateAsync(message.Data); 52 | break; 53 | 54 | case "getLogs": 55 | await HandleGetLogsAsync(message.Data); 56 | break; 57 | 58 | case "handshake": 59 | await HandleHandshakeAsync(message.Data); 60 | break; 61 | 62 | case "getSceneInfo": 63 | await HandleGetSceneInfoAsync(message.Data); 64 | break; 65 | 66 | case "getGameObjectsInfo": 67 | await HandleGetGameObjectsInfoAsync(message.Data); 68 | break; 69 | 70 | case "ping": // Renamed from 'heartbeat' to 'ping' to match protocol 71 | await HandlePingAsync(message.Data); 72 | break; 73 | 74 | default: 75 | Debug.LogWarning($"[MCP] Unknown message type: {message.Type}"); 76 | break; 77 | } 78 | } 79 | catch (Exception ex) 80 | { 81 | Debug.LogError($"[MCP] Error handling message: {ex.Message}\nMessage: {messageJson}"); 82 | } 83 | } 84 | 85 | private async Task HandleHandshakeAsync(JToken data) 86 | { 87 | try 88 | { 89 | string message = data["message"]?.ToString() ?? "Server connected"; 90 | Debug.Log($"[MCP] Handshake received: {message}"); 91 | 92 | // Send a simple acknowledgment, but don't send full editor state until requested 93 | await messageSender.SendPongAsync(); 94 | } 95 | catch (Exception ex) 96 | { 97 | Debug.LogError($"[MCP] Error handling handshake: {ex.Message}"); 98 | } 99 | } 100 | 101 | // Add a ping handler to respond to server heartbeats 102 | private async Task HandlePingAsync(JToken data) 103 | { 104 | try 105 | { 106 | // Simply respond with a pong message 107 | await messageSender.SendPongAsync(); 108 | } 109 | catch (Exception ex) 110 | { 111 | Debug.LogError($"[MCP] Error handling ping: {ex.Message}"); 112 | } 113 | } 114 | 115 | private async Task HandleSelectGameObjectAsync(JToken data) 116 | { 117 | try 118 | { 119 | string objectPath = data["path"]?.ToString(); 120 | string requestId = data["requestId"]?.ToString(); 121 | 122 | if (string.IsNullOrEmpty(objectPath)) return; 123 | 124 | var obj = GameObject.Find(objectPath); 125 | if (obj != null) 126 | { 127 | Selection.activeGameObject = obj; 128 | Debug.Log($"[MCP] Selected GameObject: {objectPath}"); 129 | 130 | // If requestId was provided, send back object details 131 | if (!string.IsNullOrEmpty(requestId)) 132 | { 133 | // Use the new method to send details for a single GameObject 134 | await messageSender.SendGameObjectDetailsAsync(requestId, obj); 135 | } 136 | } 137 | else 138 | { 139 | Debug.LogWarning($"[MCP] GameObject not found: {objectPath}"); 140 | 141 | if (!string.IsNullOrEmpty(requestId)) 142 | { 143 | await messageSender.SendErrorMessageAsync("OBJECT_NOT_FOUND", $"GameObject not found: {objectPath}"); 144 | } 145 | } 146 | } 147 | catch (Exception ex) 148 | { 149 | Debug.LogError($"[MCP] Error selecting GameObject: {ex.Message}"); 150 | } 151 | } 152 | 153 | private async Task HandleTogglePlayModeAsync() 154 | { 155 | try 156 | { 157 | EditorApplication.isPlaying = !EditorApplication.isPlaying; 158 | Debug.Log($"[MCP] Toggled play mode to: {EditorApplication.isPlaying}"); 159 | 160 | // Send updated editor state after toggling play mode 161 | var editorState = dataCollector.GetEditorState(); 162 | await messageSender.SendEditorStateAsync(editorState); 163 | } 164 | catch (Exception ex) 165 | { 166 | Debug.LogError($"[MCP] Error toggling play mode: {ex.Message}"); 167 | } 168 | } 169 | 170 | private async Task HandleExecuteCommandAsync(JToken data) 171 | { 172 | try 173 | { 174 | // Support both old and new parameter naming 175 | string commandId = data["commandId"]?.ToString() ?? data["id"]?.ToString() ?? Guid.NewGuid().ToString(); 176 | string code = data["code"]?.ToString(); 177 | 178 | if (string.IsNullOrEmpty(code)) 179 | { 180 | Debug.LogWarning("[MCP] Received empty code to execute"); 181 | await messageSender.SendErrorMessageAsync("EMPTY_CODE", "Received empty code to execute"); 182 | return; 183 | } 184 | 185 | Debug.Log($"[MCP] Executing command: {commandId}\n{code}"); 186 | 187 | var result = codeExecutor.ExecuteCode(code); 188 | 189 | // Send back the results 190 | await messageSender.SendCommandResultAsync( 191 | commandId, 192 | result, 193 | codeExecutor.GetLogs(), 194 | codeExecutor.GetErrors(), 195 | codeExecutor.GetWarnings() 196 | ); 197 | 198 | Debug.Log($"[MCP] Command execution completed"); 199 | } 200 | catch (Exception ex) 201 | { 202 | Debug.LogError($"[MCP] Error executing command: {ex.Message}"); 203 | } 204 | } 205 | 206 | // Renamed from HandleGetEditorStateAsync to HandleRequestEditorStateAsync for clarity 207 | private async Task HandleRequestEditorStateAsync(JToken data) 208 | { 209 | try 210 | { 211 | // Get current editor state with enhanced project info 212 | var editorState = GetEnhancedEditorState(); 213 | 214 | // Send it to the server 215 | await messageSender.SendEditorStateAsync(editorState); 216 | } 217 | catch (Exception ex) 218 | { 219 | Debug.LogError($"[MCP] Error getting editor state: {ex.Message}"); 220 | } 221 | } 222 | 223 | // New method to get enhanced editor state with more project information 224 | private MCPEditorState GetEnhancedEditorState() 225 | { 226 | // Get base editor state from data collector 227 | var state = dataCollector.GetEditorState(); 228 | 229 | // Add additional project information 230 | EnhanceEditorStateWithProjectInfo(state); 231 | 232 | return state; 233 | } 234 | 235 | // Add additional project information to the editor state 236 | private void EnhanceEditorStateWithProjectInfo(MCPEditorState state) 237 | { 238 | try 239 | { 240 | // Add information about current rendering pipeline 241 | state.RenderPipeline = GetCurrentRenderPipeline(); 242 | 243 | // Add current build target platform 244 | state.BuildTarget = EditorUserBuildSettings.activeBuildTarget.ToString(); 245 | 246 | // Add project name 247 | state.ProjectName = Application.productName; 248 | 249 | // Add graphics API info 250 | state.GraphicsDeviceType = SystemInfo.graphicsDeviceType.ToString(); 251 | 252 | // Add Unity version 253 | state.UnityVersion = Application.unityVersion; 254 | 255 | // Add current scene name 256 | var currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); 257 | state.CurrentSceneName = currentScene.name; 258 | state.CurrentScenePath = currentScene.path; 259 | } 260 | catch (Exception ex) 261 | { 262 | Debug.LogError($"[MCP] Error enhancing editor state: {ex.Message}"); 263 | } 264 | } 265 | 266 | // Helper to determine current render pipeline 267 | private string GetCurrentRenderPipeline() 268 | { 269 | if (UnityEngine.Rendering.GraphicsSettings.defaultRenderPipeline == null) 270 | return "Built-in Render Pipeline"; 271 | 272 | var pipelineType = UnityEngine.Rendering.GraphicsSettings.defaultRenderPipeline.GetType().Name; 273 | 274 | // Try to make the name more user-friendly 275 | if (pipelineType.Contains("Universal")) 276 | return "Universal Render Pipeline (URP)"; 277 | else if (pipelineType.Contains("HD")) 278 | return "High Definition Render Pipeline (HDRP)"; 279 | else if (pipelineType.Contains("Lightweight")) 280 | return "Lightweight Render Pipeline (LWRP)"; 281 | else 282 | return pipelineType; 283 | } 284 | 285 | private async Task HandleGetLogsAsync(JToken data) 286 | { 287 | try 288 | { 289 | string requestId = data["requestId"]?.ToString() ?? Guid.NewGuid().ToString(); 290 | int count = data["count"]?.Value() ?? 50; 291 | 292 | // Get logs from collector 293 | var logs = dataCollector.GetRecentLogs(count); 294 | 295 | // Send logs back to server 296 | await messageSender.SendGetLogsResponseAsync(requestId, logs); 297 | } 298 | catch (Exception ex) 299 | { 300 | Debug.LogError($"[MCP] Error getting logs: {ex.Message}"); 301 | } 302 | } 303 | 304 | private async Task HandleGetSceneInfoAsync(JToken data) 305 | { 306 | try 307 | { 308 | string requestId = data["requestId"]?.ToString() ?? Guid.NewGuid().ToString(); 309 | string detailLevelStr = data["detailLevel"]?.ToString() ?? "RootObjectsOnly"; 310 | 311 | // Parse the detail level 312 | SceneInfoDetail detailLevel; 313 | if (!Enum.TryParse(detailLevelStr, true, out detailLevel)) 314 | { 315 | detailLevel = SceneInfoDetail.RootObjectsOnly; 316 | } 317 | 318 | // Get scene info 319 | var sceneInfo = dataCollector.GetCurrentSceneInfo(detailLevel); 320 | 321 | // Send it to the server 322 | await messageSender.SendSceneInfoAsync(requestId, sceneInfo); 323 | } 324 | catch (Exception ex) 325 | { 326 | Debug.LogError($"[MCP] Error handling getSceneInfo: {ex.Message}"); 327 | await messageSender.SendErrorMessageAsync("SCENE_INFO_ERROR", ex.Message); 328 | } 329 | } 330 | 331 | private async Task HandleGetGameObjectsInfoAsync(JToken data) 332 | { 333 | try 334 | { 335 | string requestId = data["requestId"]?.ToString() ?? Guid.NewGuid().ToString(); 336 | string detailLevelStr = data["detailLevel"]?.ToString() ?? "BasicInfo"; 337 | 338 | // Get the list of instance IDs 339 | int[] instanceIDs; 340 | if (data["instanceIDs"] != null && data["instanceIDs"].Type == JTokenType.Array) 341 | { 342 | instanceIDs = data["instanceIDs"].ToObject(); 343 | } 344 | else 345 | { 346 | await messageSender.SendErrorMessageAsync("INVALID_PARAMS", "instanceIDs array is required"); 347 | return; 348 | } 349 | 350 | // Parse the detail level 351 | GameObjectInfoDetail detailLevel; 352 | if (!Enum.TryParse(detailLevelStr, true, out detailLevel)) 353 | { 354 | detailLevel = GameObjectInfoDetail.BasicInfo; 355 | } 356 | 357 | // Get game object details 358 | var gameObjectDetails = dataCollector.GetGameObjectsInfo(instanceIDs, detailLevel); 359 | 360 | // Send to server 361 | await messageSender.SendGameObjectsDetailsAsync(requestId, gameObjectDetails); 362 | } 363 | catch (Exception ex) 364 | { 365 | Debug.LogError($"[MCP] Error handling getGameObjectsInfo: {ex.Message}"); 366 | await messageSender.SendErrorMessageAsync("GAME_OBJECT_INFO_ERROR", ex.Message); 367 | } 368 | } 369 | } 370 | 371 | internal class MCPMessage 372 | { 373 | [JsonProperty("type")] 374 | public string Type { get; set; } 375 | 376 | [JsonProperty("data")] 377 | public JToken Data { get; set; } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /Editor/MCPMessageHandler.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8be90baad5eb48f4ab071d08051187ae -------------------------------------------------------------------------------- /Editor/MCPMessageSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json; 5 | using UnityEngine; 6 | using System.Linq; 7 | 8 | namespace Plugins.GamePilot.Editor.MCP 9 | { 10 | public class MCPMessageSender 11 | { 12 | private readonly MCPConnectionManager connectionManager; 13 | 14 | public MCPMessageSender(MCPConnectionManager connectionManager) 15 | { 16 | this.connectionManager = connectionManager ?? throw new ArgumentNullException(nameof(connectionManager)); 17 | } 18 | 19 | public async Task SendEditorStateAsync(MCPEditorState state) 20 | { 21 | if (state == null) return; 22 | if (!connectionManager.IsConnected) return; 23 | 24 | try 25 | { 26 | var message = JsonConvert.SerializeObject(new 27 | { 28 | type = "editorState", 29 | data = state 30 | }, new JsonSerializerSettings 31 | { 32 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore 33 | }); 34 | 35 | await connectionManager.SendMessageAsync(message); 36 | } 37 | catch (Exception ex) 38 | { 39 | Debug.LogError($"[MCP] Error sending editor state: {ex.Message}"); 40 | } 41 | } 42 | 43 | public async Task SendLogEntryAsync(LogEntry logEntry) 44 | { 45 | if (logEntry == null) return; 46 | if (!connectionManager.IsConnected) return; 47 | 48 | try 49 | { 50 | var message = JsonConvert.SerializeObject(new 51 | { 52 | type = "log", 53 | data = new 54 | { 55 | message = logEntry.Message, 56 | stackTrace = logEntry.StackTrace, 57 | logType = logEntry.Type.ToString(), 58 | timestamp = logEntry.Timestamp 59 | } 60 | }); 61 | 62 | await connectionManager.SendMessageAsync(message); 63 | } 64 | catch (Exception ex) 65 | { 66 | Debug.LogError($"[MCP] Error sending log entry: {ex.Message}"); 67 | } 68 | } 69 | 70 | public async Task SendCommandResultAsync(string commandId, object result, 71 | IEnumerable logs, IEnumerable errors, IEnumerable warnings) 72 | { 73 | if (!connectionManager.IsConnected) return; 74 | 75 | try 76 | { 77 | var message = JsonConvert.SerializeObject(new 78 | { 79 | type = "commandResult", 80 | data = new 81 | { 82 | commandId, 83 | result = result, 84 | logs = logs ?? Array.Empty(), 85 | errors = errors ?? Array.Empty(), 86 | warnings = warnings ?? Array.Empty(), 87 | executionSuccess = errors == null || !errors.GetEnumerator().MoveNext() 88 | } 89 | }); 90 | 91 | await connectionManager.SendMessageAsync(message); 92 | } 93 | catch (Exception ex) 94 | { 95 | Debug.LogError($"[MCP] Error sending command result: {ex.Message}"); 96 | } 97 | } 98 | 99 | public async Task SendErrorMessageAsync(string errorCode, string errorMessage) 100 | { 101 | if (!connectionManager.IsConnected) return; 102 | 103 | try 104 | { 105 | var message = JsonConvert.SerializeObject(new 106 | { 107 | type = "error", 108 | data = new 109 | { 110 | code = errorCode, 111 | message = errorMessage, 112 | timestamp = DateTime.UtcNow 113 | } 114 | }); 115 | 116 | await connectionManager.SendMessageAsync(message); 117 | } 118 | catch (Exception ex) 119 | { 120 | Debug.LogError($"[MCP] Error sending error message: {ex.Message}"); 121 | } 122 | } 123 | 124 | public async Task SendGetLogsResponseAsync(string requestId, LogEntry[] logs) 125 | { 126 | if (!connectionManager.IsConnected) return; 127 | 128 | try 129 | { 130 | var message = JsonConvert.SerializeObject(new 131 | { 132 | type = "logsResponse", 133 | data = new 134 | { 135 | requestId, 136 | logs, 137 | timestamp = DateTime.UtcNow 138 | } 139 | }); 140 | 141 | await connectionManager.SendMessageAsync(message); 142 | } 143 | catch (Exception ex) 144 | { 145 | Debug.LogError($"[MCP] Error sending logs response: {ex.Message}"); 146 | } 147 | } 148 | 149 | public async Task SendSceneInfoAsync(string requestId, MCPSceneInfo sceneInfo) 150 | { 151 | if (!connectionManager.IsConnected) return; 152 | 153 | try 154 | { 155 | var message = JsonConvert.SerializeObject(new 156 | { 157 | type = "sceneInfo", 158 | data = new 159 | { 160 | requestId, 161 | sceneInfo, 162 | timestamp = DateTime.UtcNow 163 | } 164 | }, new JsonSerializerSettings 165 | { 166 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore 167 | }); 168 | 169 | await connectionManager.SendMessageAsync(message); 170 | } 171 | catch (Exception ex) 172 | { 173 | Debug.LogError($"[MCP] Error sending scene info: {ex.Message}"); 174 | } 175 | } 176 | 177 | public async Task SendGameObjectsDetailsAsync(string requestId, List gameObjectDetails) 178 | { 179 | if (!connectionManager.IsConnected) return; 180 | 181 | try 182 | { 183 | var message = JsonConvert.SerializeObject(new 184 | { 185 | type = "gameObjectsDetails", 186 | data = new 187 | { 188 | requestId, 189 | gameObjectDetails, 190 | count = gameObjectDetails?.Count ?? 0, 191 | timestamp = DateTime.UtcNow 192 | } 193 | }, new JsonSerializerSettings 194 | { 195 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore 196 | }); 197 | 198 | await connectionManager.SendMessageAsync(message); 199 | } 200 | catch (Exception ex) 201 | { 202 | Debug.LogError($"[MCP] Error sending game objects details: {ex.Message}"); 203 | } 204 | } 205 | 206 | // Add new method to send pong message back to the server 207 | public async Task SendPongAsync() 208 | { 209 | if (!connectionManager.IsConnected) return; 210 | 211 | try 212 | { 213 | var message = JsonConvert.SerializeObject(new 214 | { 215 | type = "pong", 216 | data = new 217 | { 218 | timestamp = DateTime.UtcNow.Ticks 219 | } 220 | }); 221 | 222 | await connectionManager.SendMessageAsync(message); 223 | } 224 | catch (Exception ex) 225 | { 226 | Debug.LogError($"[MCP] Error sending pong message: {ex.Message}"); 227 | } 228 | } 229 | 230 | // Add new method to handle single GameObject details request 231 | public async Task SendGameObjectDetailsAsync(string requestId, GameObject gameObject) 232 | { 233 | if (!connectionManager.IsConnected || gameObject == null) return; 234 | 235 | try 236 | { 237 | // Create a list with a single game object detail 238 | var gameObjectDetail = new MCPGameObjectDetail 239 | { 240 | Name = gameObject.name, 241 | InstanceID = gameObject.GetInstanceID(), 242 | Path = GetGameObjectPath(gameObject), 243 | Active = gameObject.activeSelf, 244 | ActiveInHierarchy = gameObject.activeInHierarchy, 245 | Tag = gameObject.tag, 246 | Layer = gameObject.layer, 247 | LayerName = LayerMask.LayerToName(gameObject.layer), 248 | IsStatic = gameObject.isStatic, 249 | Transform = new MCPTransformInfo 250 | { 251 | Position = gameObject.transform.position, 252 | Rotation = gameObject.transform.rotation.eulerAngles, 253 | LocalPosition = gameObject.transform.localPosition, 254 | LocalRotation = gameObject.transform.localRotation.eulerAngles, 255 | LocalScale = gameObject.transform.localScale 256 | }, 257 | Components = gameObject.GetComponents() 258 | .Where(c => c != null) 259 | .Select(c => new MCPComponentInfo 260 | { 261 | Type = c.GetType().Name, 262 | IsEnabled = GetComponentEnabled(c), 263 | InstanceID = c.GetInstanceID() 264 | }) 265 | .ToList() 266 | }; 267 | 268 | var details = new List { gameObjectDetail }; 269 | 270 | // Use the existing method to send the details 271 | await SendGameObjectsDetailsAsync(requestId, details); 272 | } 273 | catch (Exception ex) 274 | { 275 | Debug.LogError($"[MCP] Error sending game object details: {ex.Message}"); 276 | await SendErrorMessageAsync("GAME_OBJECT_DETAIL_ERROR", ex.Message); 277 | } 278 | } 279 | 280 | private string GetGameObjectPath(GameObject obj) 281 | { 282 | if (obj == null) return string.Empty; 283 | 284 | string path = obj.name; 285 | var parent = obj.transform.parent; 286 | 287 | while (parent != null) 288 | { 289 | path = parent.name + "/" + path; 290 | parent = parent.parent; 291 | } 292 | 293 | return path; 294 | } 295 | 296 | private bool GetComponentEnabled(Component component) 297 | { 298 | // Try to check if component is enabled (for components that support it) 299 | try 300 | { 301 | if (component is Behaviour behaviour) 302 | return behaviour.enabled; 303 | 304 | if (component is Renderer renderer) 305 | return renderer.enabled; 306 | 307 | if (component is Collider collider) 308 | return collider.enabled; 309 | } 310 | catch 311 | { 312 | // Ignore any exceptions 313 | } 314 | 315 | // Default to true for components that don't have an enabled property 316 | return true; 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /Editor/MCPMessageSender.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 556cbbf6d80226346b1becf7cedb0460 -------------------------------------------------------------------------------- /Editor/Models.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7a07dbb1b6bbba6488c9a1cfd64d126a 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Models/MCPEditorState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace Plugins.GamePilot.Editor.MCP 6 | { 7 | [Serializable] 8 | public class MCPEditorState 9 | { 10 | [JsonProperty("activeGameObjects")] 11 | public string[] ActiveGameObjects { get; set; } = new string[0]; 12 | 13 | [JsonProperty("selectedObjects")] 14 | public string[] SelectedObjects { get; set; } = new string[0]; 15 | 16 | [JsonProperty("playModeState")] 17 | public string PlayModeState { get; set; } = "Stopped"; 18 | 19 | [JsonProperty("sceneHierarchy")] 20 | public List SceneHierarchy { get; set; } = new List(); 21 | 22 | // Removed ProjectStructure property 23 | 24 | [JsonProperty("timestamp")] 25 | public DateTime Timestamp { get; set; } = DateTime.UtcNow; 26 | 27 | // Enhanced project information properties 28 | [JsonProperty("renderPipeline")] 29 | public string RenderPipeline { get; set; } = "Unknown"; 30 | 31 | [JsonProperty("buildTarget")] 32 | public string BuildTarget { get; set; } = "Unknown"; 33 | 34 | [JsonProperty("projectName")] 35 | public string ProjectName { get; set; } = "Unknown"; 36 | 37 | [JsonProperty("graphicsDeviceType")] 38 | public string GraphicsDeviceType { get; set; } = "Unknown"; 39 | 40 | [JsonProperty("unityVersion")] 41 | public string UnityVersion { get; set; } = "Unknown"; 42 | 43 | [JsonProperty("currentSceneName")] 44 | public string CurrentSceneName { get; set; } = "Unknown"; 45 | 46 | [JsonProperty("currentScenePath")] 47 | public string CurrentScenePath { get; set; } = "Unknown"; 48 | 49 | [JsonProperty("availableMenuItems")] 50 | public List AvailableMenuItems { get; set; } = new List(); 51 | } 52 | 53 | [Serializable] 54 | public class MCPGameObjectInfo 55 | { 56 | [JsonProperty("name")] 57 | public string Name { get; set; } 58 | 59 | [JsonProperty("path")] 60 | public string Path { get; set; } 61 | 62 | [JsonProperty("components")] 63 | public string[] Components { get; set; } = new string[0]; 64 | 65 | [JsonProperty("children")] 66 | public List Children { get; set; } = new List(); 67 | 68 | [JsonProperty("active")] 69 | public bool Active { get; set; } = true; 70 | 71 | [JsonProperty("layer")] 72 | public int Layer { get; set; } 73 | 74 | [JsonProperty("tag")] 75 | public string Tag { get; set; } 76 | } 77 | 78 | // Removed MCPProjectStructure class 79 | 80 | [Serializable] 81 | public class LogEntry 82 | { 83 | [JsonProperty("message")] 84 | public string Message { get; set; } 85 | 86 | [JsonProperty("stackTrace")] 87 | public string StackTrace { get; set; } 88 | 89 | [JsonProperty("type")] 90 | public UnityEngine.LogType Type { get; set; } 91 | 92 | [JsonProperty("timestamp")] 93 | public DateTime Timestamp { get; set; } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Editor/Models/MCPEditorState.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f8471fc245729a441bfb6196725f4508 -------------------------------------------------------------------------------- /Editor/Models/MCPSceneInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using UnityEngine; 5 | 6 | namespace Plugins.GamePilot.Editor.MCP 7 | { 8 | [Serializable] 9 | public class MCPSceneInfo 10 | { 11 | [JsonProperty("name")] 12 | public string Name { get; set; } 13 | 14 | [JsonProperty("path")] 15 | public string Path { get; set; } 16 | 17 | [JsonProperty("isDirty")] 18 | public bool IsDirty { get; set; } 19 | 20 | [JsonProperty("rootCount")] 21 | public int RootCount { get; set; } 22 | 23 | [JsonProperty("rootObjects")] 24 | public List RootObjects { get; set; } = new List(); 25 | 26 | [JsonProperty("errorMessage")] 27 | public string ErrorMessage { get; set; } 28 | } 29 | 30 | [Serializable] 31 | public class MCPGameObjectReference 32 | { 33 | [JsonProperty("name")] 34 | public string Name { get; set; } 35 | 36 | [JsonProperty("instanceID")] 37 | public int InstanceID { get; set; } 38 | 39 | [JsonProperty("path")] 40 | public string Path { get; set; } 41 | 42 | [JsonProperty("active")] 43 | public bool Active { get; set; } 44 | 45 | [JsonProperty("childCount")] 46 | public int ChildCount { get; set; } 47 | 48 | [JsonProperty("children")] 49 | public List Children { get; set; } 50 | } 51 | 52 | [Serializable] 53 | public class MCPGameObjectDetail 54 | { 55 | [JsonProperty("name")] 56 | public string Name { get; set; } 57 | 58 | [JsonProperty("instanceID")] 59 | public int InstanceID { get; set; } 60 | 61 | [JsonProperty("path")] 62 | public string Path { get; set; } 63 | 64 | [JsonProperty("active")] 65 | public bool Active { get; set; } 66 | 67 | [JsonProperty("activeInHierarchy")] 68 | public bool ActiveInHierarchy { get; set; } 69 | 70 | [JsonProperty("tag")] 71 | public string Tag { get; set; } 72 | 73 | [JsonProperty("layer")] 74 | public int Layer { get; set; } 75 | 76 | [JsonProperty("layerName")] 77 | public string LayerName { get; set; } 78 | 79 | [JsonProperty("isStatic")] 80 | public bool IsStatic { get; set; } 81 | 82 | [JsonProperty("transform")] 83 | public MCPTransformInfo Transform { get; set; } 84 | 85 | [JsonProperty("components")] 86 | public List Components { get; set; } 87 | 88 | [JsonProperty("children")] 89 | public List Children { get; set; } 90 | } 91 | 92 | [Serializable] 93 | public class MCPTransformInfo 94 | { 95 | [JsonProperty("position")] 96 | public Vector3 Position { get; set; } 97 | 98 | [JsonProperty("rotation")] 99 | public Vector3 Rotation { get; set; } 100 | 101 | [JsonProperty("localPosition")] 102 | public Vector3 LocalPosition { get; set; } 103 | 104 | [JsonProperty("localRotation")] 105 | public Vector3 LocalRotation { get; set; } 106 | 107 | [JsonProperty("localScale")] 108 | public Vector3 LocalScale { get; set; } 109 | } 110 | 111 | [Serializable] 112 | public class MCPComponentInfo 113 | { 114 | [JsonProperty("type")] 115 | public string Type { get; set; } 116 | 117 | [JsonProperty("isEnabled")] 118 | public bool IsEnabled { get; set; } 119 | 120 | [JsonProperty("instanceID")] 121 | public int InstanceID { get; set; } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Editor/Models/MCPSceneInfo.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 060bbd8d292107b4985a4ac2e0d0b54f -------------------------------------------------------------------------------- /Editor/UI.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bb27f795307eaf744bf8f563a665e71e 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/UI/MCPDebugSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 5010, 3 | "autoReconnect": false, 4 | "globalLoggingEnabled": false, 5 | "componentLoggingEnabled": {} 6 | } -------------------------------------------------------------------------------- /Editor/UI/MCPDebugSettings.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: becf75c31d9db0f478156c04914b3b8a 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/UI/MCPDebugWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8d4efbc0c4ef1c74dad2dad6d7dcd1c5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: 7 | - m_ViewDataDictionary: {instanceID: 0} 8 | - uxml: {fileID: 9197481963319205126, guid: c2a8d11fcae286b40b9c1c222fbcdb78, type: 3} 9 | - uss: {fileID: 7433441132597879392, guid: 87ff82e34d02e3440b0377c8b2584d6b, type: 3} 10 | - m_VisualTreeAsset: {instanceID: 0} 11 | executionOrder: 0 12 | icon: {instanceID: 0} 13 | userData: 14 | assetBundleName: 15 | assetBundleVariant: 16 | -------------------------------------------------------------------------------- /Editor/UI/MCPDebugWindow.uss: -------------------------------------------------------------------------------- 1 | .debug-window-root { 2 | padding: 10px; 3 | } 4 | 5 | .header-container { 6 | background-color: rgb(60, 60, 60); 7 | border-radius: 5px; 8 | padding: 10px; 9 | margin-bottom: 15px; 10 | } 11 | 12 | .header-title { 13 | font-size: 20px; 14 | -unity-font-style: bold; 15 | color: rgb(68, 138, 255); 16 | margin-bottom: 5px; 17 | } 18 | 19 | .header-subtitle { 20 | font-size: 12px; 21 | color: rgb(180, 180, 180); 22 | -unity-font-style: italic; 23 | } 24 | 25 | .section-container { 26 | background-color: rgb(45, 45, 45); 27 | border-radius: 5px; 28 | padding: 10px; 29 | margin-top: 10px; 30 | margin-bottom: 10px; 31 | } 32 | 33 | .section-header { 34 | font-size: 16px; 35 | -unity-font-style: bold; 36 | margin-bottom: 5px; 37 | border-bottom-width: 1px; 38 | border-bottom-color: rgb(80, 80, 80); 39 | padding-bottom: 5px; 40 | color: rgb(200, 200, 200); 41 | } 42 | 43 | .control-button { 44 | margin: 5px; 45 | padding: 5px 10px; 46 | min-width: 100px; 47 | border-radius: 3px; 48 | } 49 | 50 | .connect-button { 51 | background-color: rgb(68, 138, 255); 52 | color: white; 53 | -unity-font-style: bold; 54 | border-width: 0; 55 | } 56 | 57 | .connect-button:hover { 58 | background-color: rgb(100, 160, 255); 59 | } 60 | 61 | .connect-button:active { 62 | background-color: rgb(40, 110, 220); 63 | } 64 | 65 | .disconnect-button { 66 | background-color: rgb(160, 60, 60); 67 | color: white; 68 | -unity-font-style: bold; 69 | border-width: 0; 70 | } 71 | 72 | .disconnect-button:hover { 73 | background-color: rgb(180, 80, 80); 74 | } 75 | 76 | .disconnect-button:active { 77 | background-color: rgb(140, 40, 40); 78 | } 79 | 80 | .status-label { 81 | font-size: 16px; 82 | -unity-font-style: bold; 83 | margin: 5px 0; 84 | } 85 | 86 | .status-connected { 87 | color: rgb(100, 200, 100); 88 | } 89 | 90 | .status-disconnected { 91 | color: rgb(200, 100, 100); 92 | } 93 | 94 | .status-connecting { 95 | color: rgb(200, 200, 100); 96 | } 97 | 98 | .info-grid { 99 | margin-top: 10px; 100 | } 101 | 102 | .info-row { 103 | flex-direction: row; 104 | margin: 3px 0; 105 | } 106 | 107 | .info-label { 108 | width: 120px; 109 | -unity-font-style: bold; 110 | } 111 | 112 | .info-value { 113 | flex-grow: 1; 114 | } 115 | 116 | .toggle-control { 117 | margin: 5px 0; 118 | } 119 | 120 | .statistics-container { 121 | flex-direction: row; 122 | flex-wrap: wrap; 123 | } 124 | 125 | .statistic-box { 126 | width: 100px; 127 | height: 60px; 128 | margin: 5px; 129 | padding: 5px; 130 | background-color: rgb(55, 55, 55); 131 | border-radius: 3px; 132 | align-items: center; 133 | justify-content: center; 134 | } 135 | 136 | .statistic-label { 137 | font-size: 10px; 138 | -unity-text-align: middle-center; 139 | color: rgb(180, 180, 180); 140 | } 141 | 142 | .statistic-value { 143 | font-size: 24px; 144 | -unity-font-style: bold; 145 | -unity-text-align: middle-center; 146 | color: rgb(68, 138, 255); 147 | } 148 | 149 | .server-url-label { 150 | -unity-font-style: bold; 151 | width: 80px; 152 | } 153 | 154 | .server-url-field { 155 | flex-grow: 1; 156 | margin-left: 10px; 157 | } 158 | -------------------------------------------------------------------------------- /Editor/UI/MCPDebugWindow.uss.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 87ff82e34d02e3440b0377c8b2584d6b 3 | ScriptedImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 2 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} 11 | disableValidation: 0 12 | -------------------------------------------------------------------------------- /Editor/UI/MCPDebugWindow.uxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Editor/UI/MCPDebugWindow.uxml.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c2a8d11fcae286b40b9c1c222fbcdb78 3 | ScriptedImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 2 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Singularity 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 36fa550ef45a23249b682696f459a3df 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Advacned Unity MCP Integration 2 | 3 | [![MCP](https://badge.mcpx.dev)](https://modelcontextprotocol.io/introduction) 4 | [![smithery badge](https://smithery.ai/badge/@quazaai/unitymcpintegration)](https://smithery.ai/server/@quazaai/unitymcpintegration) 5 | [![Unity](https://img.shields.io/badge/Unity-2021.3%2B-green?logo=https://w7.pngwing.com/pngs/426/535/png-transparent-unity-new-logo-tech-companies-thumbnail.png)](https://unity.com) 6 | [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green)](https://nodejs.org) 7 | [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)](https://www.typescriptlang.org) 8 | [![WebSockets](https://img.shields.io/badge/WebSockets-API-orange)](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) 9 | 10 | [![Stars](https://img.shields.io/github/stars/quazaai/UnityMCPIntegration)](https://github.com/quazaai/UnityMCPIntegration/stargazers) 11 | [![Forks](https://img.shields.io/github/forks/quazaai/UnityMCPIntegration)](https://github.com/quazaai/UnityMCPIntegration/network/members) 12 | [![License](https://img.shields.io/github/license/quazaai/UnityMCPIntegration)](https://github.com/quazaai/UnityMCPIntegration/blob/main/LICENSE) 13 | 14 |
15 | Unity MCP Inspector 16 |
17 | 18 | This package provides a seamless integration between [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) and Unity Editor, allowing AI assistants to understand and interact with your Unity projects in real-time. With this integration, AI assistants can access information about your scene hierarchy, project settings, and execute code directly in the Unity Editor context. 19 | 20 | ## 📚 Features 21 | - Browse and manipulate project files directly 22 | - Access real-time information about your Unity project 23 | - Understand your scene hierarchy and game objects 24 | - Execute C# code directly in the Unity Editor 25 | - Monitor logs and errors 26 | - Control the Editor's play mode 27 | - Wait For Code Execution 28 | 29 | 30 | 31 | 32 | 33 | ## 🚀 Getting Started 34 | 35 | ### Prerequisites 36 | 37 | - Unity 2021.3 or later 38 | - Node.js 18+ (for running the MCP server) 39 | 40 | ### Installation 41 | 42 | #### 1. Install Unity Package 43 | 44 | You have several options to install the Unity package: 45 | 46 | **Option A: Package Manager (Git URL)** 47 | 1. Open the Unity Package Manager (`Window > Package Manager`) 48 | 2. Click the `+` button and select `Add package from git URL...` 49 | 3. Enter the repository URL: `https://github.com/quazaai/UnityMCPIntegration.git` 50 | 4. Click `Add` 51 | 52 | **Option B: Import Custom Package** 53 | 1. Clone this repository or [download it as a unityPackage](https://github.com/quazaai/UnityMCPIntegration/releases) 54 | 2. In Unity, go to `Assets > Import Package > Custom Package` 55 | 3. Select the `UnityMCPIntegration.unitypackage` file 56 | 57 | 58 | 59 | #### 2. Set up the MCP Server 60 | 61 | You have two options to run the MCP server: 62 | 63 | **Option A: Run the server directly** 64 | 65 | 1. Navigate to the `mcpServer (likely \Library\PackageCache\com.quaza.unitymcp@d2b8f1260bca\mcpServer\)` directory 66 | 2. Install dependencies: 67 | ``` 68 | npm install 69 | ``` 70 | 3. Run the server: 71 | ``` 72 | node build/index.js 73 | ``` 74 | 75 | **Option B: Add to MCP Host configuration** 76 | 77 | Add the server to your MCP Host configuration for Claude Desktop, Custom Implementation etc 78 | 79 | ```json 80 | { 81 | "mcpServers": { 82 | "unity-mcp-server": { 83 | "command": "node", 84 | "args": [ 85 | "path-to-project>\\Library\\PackageCache\\com.quaza.unitymcp@d2b8f1260bca\\mcpServer\\mcpServer\\build\\index.js" 86 | ], 87 | "env": { 88 | "MCP_WEBSOCKET_PORT": "5010" 89 | } 90 | } 91 | } 92 | } 93 | ``` 94 | ### Demo Video 95 | [![YouTube](http://i.ytimg.com/vi/GxTlahBXs74/hqdefault.jpg)](https://www.youtube.com/watch?v=GxTlahBXs74) 96 | 97 | ### Installing via Smithery 98 | 99 | To install Unity MCP Integration for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@quazaai/unitymcpintegration): 100 | 101 | ```bash 102 | npx -y @smithery/cli install @quazaai/unitymcpintegration --client claude 103 | ``` 104 | 105 | ### 🔧 Usage 106 | 107 | #### Debugging and Monitoring 108 | 109 | You can open the MCP Debug window in Unity to monitor the connection and test features: 110 | 111 | 1. Go to `Window > MCP Debug` 112 | 2. Use the debug window to: 113 | - Check connection status 114 | - Test code execution 115 | - View logs 116 | - Monitor events 117 | 118 | #### Available Tools 119 | 120 | The Unity MCP integration provides several tools to AI assistants: 121 | 122 | ##### Unity Editor Tools 123 | - **get_editor_state**: Get comprehensive information about the Unity project and editor state 124 | - **get_current_scene_info**: Get detailed information about the current scene 125 | - **get_game_objects_info**: Get information about specific GameObjects in the scene 126 | - **execute_editor_command**: Execute C# code directly in the Unity Editor 127 | - **get_logs**: Retrieve and filter Unity console logs 128 | - **verify_connection**: Check if there's an active connection to Unity Editor 129 | 130 | ##### Filesystem Tools 131 | - **read_file**: Read contents of a file in your Unity project 132 | - **read_multiple_files**: Read multiple files at once 133 | - **write_file**: Create or overwrite a file with new content 134 | - **edit_file**: Make targeted edits to existing files with diff preview 135 | - **list_directory**: Get a listing of files and folders in a directory 136 | - **directory_tree**: Get a hierarchical view of directories and files 137 | - **search_files**: Find files matching a search pattern 138 | - **get_file_info**: Get metadata about a specific file or directory 139 | - **find_assets_by_type**: Find all assets of a specific type (e.g. Material, Prefab) 140 | - **list_scripts**: Get a listing of all C# scripts in the project 141 | 142 | File paths can be absolute or relative to the Unity project's Assets folder. For example, `"Scenes/MyScene.unity"` refers to `/Assets/Scenes/MyScene.unity`. 143 | 144 | ## 🛠️ Architecture 145 | 146 | The integration consists of two main components: 147 | 148 | 1. **Unity Plugin (C#)**: Resides in the Unity Editor and provides access to Editor APIs 149 | 2. **MCP Server (TypeScript/Node.js)**: Implements the MCP protocol and communicates with the Unity plugin 150 | 151 | Communication between them happens via WebSocket, transferring JSON messages for commands and data. 152 | 153 | ## File System Access 154 | 155 | The Unity MCP integration now includes powerful filesystem tools that allow AI assistants to: 156 | 157 | - Browse, read, and edit files in your Unity project 158 | - Create new files and directories 159 | - Search for specific files or asset types 160 | - Analyze your project structure 161 | - Make targeted code changes with diff previews 162 | 163 | All file operations are restricted to the Unity project directory for security. The system intelligently handles both absolute and relative paths, always resolving them relative to your project's Assets folder for convenience. 164 | 165 | Example usages: 166 | - Get a directory listing: `list_directory(path: "Scenes")` 167 | - Read a script file: `read_file(path: "Scripts/Player.cs")` 168 | - Edit a configuration file: `edit_file(path: "Resources/config.json", edits: [{oldText: "value: 10", newText: "value: 20"}], dryRun: true)` 169 | - Find all materials: `find_assets_by_type(assetType: "Material")` 170 | 171 | ## 👥 Contributing 172 | 173 | Contributions are welcome! Here's how you can contribute: 174 | 175 | 1. Fork the repository 176 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 177 | 3. Make your changes 178 | 4. Commit your changes (`git commit -m 'Add some amazing feature'`) 179 | 5. Push to the branch (`git push origin feature/amazing-feature`) 180 | 6. Open a Pull Request 181 | 182 | ### Development Setup 183 | 184 | **Unity Side**: 185 | - Open the project in Unity 186 | - Modify the C# scripts in the `UnityMCPConnection/Editor` directory 187 | 188 | **Server Side**: 189 | - Navigate to the `mcpServer` directory 190 | - Install dependencies: `npm install` 191 | - Make changes to the TypeScript files in the `src` directory 192 | - Build the server: `npm run build` 193 | - Run the server: `node build/index.js` 194 | 195 | ## 📄 License 196 | 197 | This project is licensed under the MIT License - see the LICENSE file for details. 198 | 199 | ## 📞 Support 200 | 201 | If you encounter any issues or have questions, please file an issue on the GitHub repository. 202 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ea08510c58a18af44b3e2a42dc8cb823 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpInspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quazaai/UnityMCPIntegration/d1515b326e2fc14b39de5a08534159ba29963c7d/mcpInspector.png -------------------------------------------------------------------------------- /mcpInspector.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2350be3e919d6464baf97e807b701889 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 13 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | flipGreenChannel: 0 24 | isReadable: 0 25 | streamingMipmaps: 0 26 | streamingMipmapsPriority: 0 27 | vTOnly: 0 28 | ignoreMipmapLimit: 0 29 | grayScaleToAlpha: 0 30 | generateCubemap: 6 31 | cubemapConvolution: 0 32 | seamlessCubemap: 0 33 | textureFormat: 1 34 | maxTextureSize: 2048 35 | textureSettings: 36 | serializedVersion: 2 37 | filterMode: 1 38 | aniso: 1 39 | mipBias: 0 40 | wrapU: 0 41 | wrapV: 0 42 | wrapW: 0 43 | nPOTScale: 1 44 | lightmap: 0 45 | compressionQuality: 50 46 | spriteMode: 0 47 | spriteExtrude: 1 48 | spriteMeshType: 1 49 | alignment: 0 50 | spritePivot: {x: 0.5, y: 0.5} 51 | spritePixelsToUnits: 100 52 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 53 | spriteGenerateFallbackPhysicsShape: 1 54 | alphaUsage: 1 55 | alphaIsTransparency: 0 56 | spriteTessellationDetail: -1 57 | textureType: 0 58 | textureShape: 1 59 | singleChannelComponent: 0 60 | flipbookRows: 1 61 | flipbookColumns: 1 62 | maxTextureSizeSet: 0 63 | compressionQualitySet: 0 64 | textureFormatSet: 0 65 | ignorePngGamma: 0 66 | applyGammaDecoding: 0 67 | swizzle: 50462976 68 | cookieLightType: 0 69 | platformSettings: 70 | - serializedVersion: 4 71 | buildTarget: DefaultTexturePlatform 72 | maxTextureSize: 2048 73 | resizeAlgorithm: 0 74 | textureFormat: -1 75 | textureCompression: 1 76 | compressionQuality: 50 77 | crunchedCompression: 0 78 | allowsAlphaSplitting: 0 79 | overridden: 0 80 | ignorePlatformSupport: 0 81 | androidETC2FallbackOverride: 0 82 | forceMaximumCompressionQuality_BC6H_BC7: 0 83 | - serializedVersion: 4 84 | buildTarget: Standalone 85 | maxTextureSize: 2048 86 | resizeAlgorithm: 0 87 | textureFormat: -1 88 | textureCompression: 1 89 | compressionQuality: 50 90 | crunchedCompression: 0 91 | allowsAlphaSplitting: 0 92 | overridden: 0 93 | ignorePlatformSupport: 0 94 | androidETC2FallbackOverride: 0 95 | forceMaximumCompressionQuality_BC6H_BC7: 0 96 | - serializedVersion: 4 97 | buildTarget: Android 98 | maxTextureSize: 2048 99 | resizeAlgorithm: 0 100 | textureFormat: -1 101 | textureCompression: 1 102 | compressionQuality: 50 103 | crunchedCompression: 0 104 | allowsAlphaSplitting: 0 105 | overridden: 0 106 | ignorePlatformSupport: 0 107 | androidETC2FallbackOverride: 0 108 | forceMaximumCompressionQuality_BC6H_BC7: 0 109 | - serializedVersion: 4 110 | buildTarget: WindowsStoreApps 111 | maxTextureSize: 2048 112 | resizeAlgorithm: 0 113 | textureFormat: -1 114 | textureCompression: 1 115 | compressionQuality: 50 116 | crunchedCompression: 0 117 | allowsAlphaSplitting: 0 118 | overridden: 0 119 | ignorePlatformSupport: 0 120 | androidETC2FallbackOverride: 0 121 | forceMaximumCompressionQuality_BC6H_BC7: 0 122 | spriteSheet: 123 | serializedVersion: 2 124 | sprites: [] 125 | outline: [] 126 | customData: 127 | physicsShape: [] 128 | bones: [] 129 | spriteID: 130 | internalID: 0 131 | vertices: [] 132 | indices: 133 | edges: [] 134 | weights: [] 135 | secondaryTextures: [] 136 | spriteCustomMetadata: 137 | entries: [] 138 | nameFileIdTable: {} 139 | mipmapLimitGroupName: 140 | pSDRemoveMatte: 0 141 | userData: 142 | assetBundleName: 143 | assetBundleVariant: 144 | -------------------------------------------------------------------------------- /mcpServer.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ae68b0e2ab9314349b780c81ef8b88eb 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /mcpServer/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .git 4 | .github 5 | .vscode 6 | *.md 7 | build/ 8 | .env 9 | -------------------------------------------------------------------------------- /mcpServer/.env.example: -------------------------------------------------------------------------------- 1 | # Unity MCP Server Configuration 2 | 3 | # WebSocket port for Unity Editor connection 4 | MCP_WEBSOCKET_PORT=5010 5 | -------------------------------------------------------------------------------- /mcpServer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:lts-alpine 3 | 4 | WORKDIR /app 5 | 6 | # Copy package.json and package-lock.json 7 | COPY package*.json ./ 8 | 9 | # Install dependencies (ignore scripts if any issues with native builds) 10 | RUN npm install --ignore-scripts 11 | 12 | # Copy the rest of the application 13 | COPY . . 14 | 15 | # Build the TypeScript source 16 | RUN npm run build 17 | 18 | # Expose PORT if needed (optional) 19 | 20 | # Start the MCP server 21 | CMD ["node", "build/index.js"] -------------------------------------------------------------------------------- /mcpServer/Dockerfile.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 09523e3291be5b84791e3cf5824f41cd 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/MCPSummary.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 4 | 5 | --- 6 | 7 | # https://modelcontextprotocol.io/introduction 8 | 9 | https://modelcontextprotocol.io/quickstart/server 10 | https://modelcontextprotocol.io/quickstart/client 11 | https://modelcontextprotocol.io/docs/concepts/tools 12 | give me summary 13 | 14 | Here is a summary of the provided pages from the Model Context Protocol (MCP) documentation: 15 | 16 | --- 17 | 18 | ### **Introduction to MCP** 19 | 20 | - MCP is an open protocol that standardizes how applications provide context to large language models (LLMs), akin to how USB-C standardizes device connectivity[^1]. 21 | - It enables: 22 | - Pre-built integrations for LLMs to connect with data and tools. 23 | - Flexibility in switching between LLM providers. 24 | - Secure data handling within user infrastructure[^1]. 25 | - MCP follows a **client-server architecture**: 26 | - **MCP Hosts**: Applications like Claude Desktop or IDEs that access data via MCP. 27 | - **MCP Clients**: Protocol clients maintaining connections with servers. 28 | - **MCP Servers**: Lightweight programs exposing specific capabilities through MCP. 29 | - **Local Data Sources** and **Remote Services**: Securely accessed by servers[^1]. 30 | 31 | --- 32 | 33 | ### **Quickstart: Server** 34 | 35 | - This guide demonstrates building an MCP weather server that provides tools for fetching weather forecasts and alerts, connecting it to a host like Claude for Desktop[^2]. 36 | - **Core Concepts**: 37 | - **Resources**: File-like data accessible by clients. 38 | - **Tools**: Functions callable by LLMs with user approval. 39 | - **Prompts**: Templates for specific tasks[^2]. 40 | - Example implementation includes: 41 | - Setting up a Python environment with the `uv` tool. 42 | - Using the National Weather Service API to fetch weather data. 43 | - Exposing tools like `get-alerts` and `get-forecast` for integration with hosts like Claude for Desktop[^2]. 44 | - The server is tested by configuring Claude for Desktop to recognize and interact with it via commands[^2]. 45 | 46 | --- 47 | 48 | ### **Quickstart: Client** 49 | 50 | - This guide explains building an LLM-powered chatbot client capable of connecting to MCP servers[^3]. 51 | - Steps include: 52 | - Setting up a Python project using `uv`. 53 | - Storing API keys securely in environment files. 54 | - Creating a client class to manage server connections and process queries[^3]. 55 | - Key functionalities: 56 | - Connecting to MCP servers via Python or JavaScript scripts. 57 | - Handling queries by leveraging available server tools and integrating responses into natural language outputs using LLMs like Claude[^3]. 58 | 59 | --- 60 | 61 | ### **Concepts: Tools** 62 | 63 | - Tools are functions exposed by MCP servers, enabling LLMs to perform specific actions. These tools are defined programmatically and can be invoked securely with user consent[^1]. 64 | - Example use cases include automating workflows, accessing external APIs, or interacting with local/remote datasets through standardized interfaces[^1]. 65 | 66 | --- 67 | 68 | This documentation provides comprehensive guidance on building both servers and clients using MCP, enabling seamless integration between LLMs and various data sources or tools. 69 | 70 |
71 | 72 | [^1]: https://modelcontextprotocol.io/introduction 73 | 74 | [^2]: https://modelcontextprotocol.io/quickstart/server 75 | 76 | [^3]: https://modelcontextprotocol.io/quickstart/client 77 | 78 | [^4]: https://modelcontextprotocol.io/introduction 79 | 80 | [^5]: https://modelcontextprotocol.io/quickstart/server 81 | 82 | [^6]: https://modelcontextprotocol.io/quickstart/client 83 | 84 | [^7]: https://modelcontextprotocol.io/docs/concepts/tools 85 | 86 | -------------------------------------------------------------------------------- /mcpServer/MCPSummary.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 71ba5f4b185cb534788668a0a3632ed0 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/build.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e7957f098400d5242b247ba4b5651075 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /mcpServer/build/filesystemTools.js.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d452cf7a25e80b84db532653e3b1f3e5 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/build/index.js: -------------------------------------------------------------------------------- 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 { WebSocketHandler } from './websocketHandler.js'; 5 | import { registerTools } from './toolDefinitions.js'; 6 | import path from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | import fs from 'fs'; 9 | class UnityMCPServer { 10 | server; 11 | wsHandler; 12 | constructor() { 13 | // Initialize MCP Server 14 | this.server = new Server({ name: 'unity-mcp-server', version: '0.2.0' }, { capabilities: { tools: {} } }); 15 | // Setup project paths and websocket 16 | const wsPort = parseInt(process.env.MCP_WEBSOCKET_PORT || '5010'); 17 | const projectRootPath = this.setupProjectPaths(); 18 | // Initialize WebSocket Handler for Unity communication 19 | this.wsHandler = new WebSocketHandler(wsPort); 20 | // Register MCP tools 21 | registerTools(this.server, this.wsHandler); 22 | // Error handling 23 | this.server.onerror = (error) => console.error('[MCP Error]', error); 24 | this.setupShutdownHandlers(); 25 | } 26 | setupProjectPaths() { 27 | const __filename = fileURLToPath(import.meta.url); 28 | const __dirname = path.dirname(__filename); 29 | console.error(`[Unity MCP] Server starting from directory: ${__dirname}`); 30 | // Get the project root path (parent of Assets) 31 | let projectRootPath = process.env.UNITY_PROJECT_PATH || this.determineUnityProjectPath(__dirname); 32 | projectRootPath = path.normalize(projectRootPath.replace(/["']/g, '')); 33 | // Make sure path ends with a directory separator 34 | if (!projectRootPath.endsWith(path.sep)) { 35 | projectRootPath += path.sep; 36 | } 37 | // Create the full path to the Assets folder 38 | const projectPath = path.join(projectRootPath, 'Assets') + path.sep; 39 | this.setupEnvironmentPath(projectRootPath, projectPath); 40 | return projectRootPath; 41 | } 42 | setupEnvironmentPath(projectRootPath, projectPath) { 43 | try { 44 | if (fs.existsSync(projectPath)) { 45 | console.error(`[Unity MCP] Using project path: ${projectPath}`); 46 | process.env.UNITY_PROJECT_PATH = projectPath; 47 | } 48 | else { 49 | console.error(`[Unity MCP] WARNING: Assets folder not found at ${projectPath}`); 50 | console.error(`[Unity MCP] Using project root instead: ${projectRootPath}`); 51 | process.env.UNITY_PROJECT_PATH = projectRootPath; 52 | } 53 | } 54 | catch (error) { 55 | console.error(`[Unity MCP] Error checking project path: ${error}`); 56 | process.env.UNITY_PROJECT_PATH = process.cwd(); 57 | } 58 | } 59 | setupShutdownHandlers() { 60 | const cleanupHandler = async () => { 61 | await this.cleanup(); 62 | process.exit(0); 63 | }; 64 | process.on('SIGINT', cleanupHandler); 65 | process.on('SIGTERM', cleanupHandler); 66 | } 67 | /** 68 | * Determine the Unity project path based on the script location 69 | */ 70 | determineUnityProjectPath(scriptDir) { 71 | scriptDir = path.normalize(scriptDir); 72 | console.error(`[Unity MCP] Script directory: ${scriptDir}`); 73 | // Case 1: Installed in Assets folder 74 | const assetsMatch = /^(.+?[\/\\]Assets)[\/\\].*$/i.exec(scriptDir); 75 | if (assetsMatch) { 76 | const projectRoot = path.dirname(assetsMatch[1]); 77 | console.error(`[Unity MCP] Detected installation in Assets folder: ${projectRoot}`); 78 | return projectRoot; 79 | } 80 | // Case 2: Installed via Package Manager 81 | const libraryMatch = /^(.+?[\/\\]Library)[\/\\]PackageCache[\/\\].*$/i.exec(scriptDir); 82 | if (libraryMatch) { 83 | const projectRoot = path.dirname(libraryMatch[1]); 84 | console.error(`[Unity MCP] Detected installation via Package Manager: ${projectRoot}`); 85 | const assetsPath = path.join(projectRoot, 'Assets'); 86 | if (fs.existsSync(assetsPath)) { 87 | return projectRoot; 88 | } 89 | } 90 | // Case 3: Check parent directories 91 | for (const dir of this.getParentDirectories(scriptDir)) { 92 | // Check if this directory is "UnityMCP" 93 | if (path.basename(dir) === 'UnityMCP') { 94 | console.error(`[Unity MCP] Found UnityMCP directory at: ${dir}`); 95 | return dir; 96 | } 97 | // Check if this directory contains an Assets folder 98 | const assetsDir = path.join(dir, 'Assets'); 99 | try { 100 | if (fs.existsSync(assetsDir) && fs.statSync(assetsDir).isDirectory()) { 101 | console.error(`[Unity MCP] Found Unity project at: ${dir}`); 102 | return dir; 103 | } 104 | } 105 | catch (e) { 106 | // Ignore errors checking directories 107 | } 108 | } 109 | // Fallback 110 | console.error('[Unity MCP] Could not detect Unity project directory. Using current directory.'); 111 | return process.cwd(); 112 | } 113 | getParentDirectories(filePath) { 114 | const result = []; 115 | const dirs = filePath.split(path.sep); 116 | for (let i = 1; i <= dirs.length; i++) { 117 | result.push(dirs.slice(0, i).join(path.sep)); 118 | } 119 | return result; 120 | } 121 | async cleanup() { 122 | console.error('Cleaning up resources...'); 123 | await this.wsHandler.close(); 124 | await this.server.close(); 125 | } 126 | async run() { 127 | const transport = new StdioServerTransport(); 128 | await this.server.connect(transport); 129 | console.error('[Unity MCP] Server running and ready to accept connections'); 130 | console.error('[Unity MCP] WebSocket server listening on port', this.wsHandler.port); 131 | } 132 | } 133 | // Start the server 134 | const server = new UnityMCPServer(); 135 | server.run().catch(err => { 136 | console.error('Fatal error in MCP server:', err); 137 | process.exit(1); 138 | }); 139 | -------------------------------------------------------------------------------- /mcpServer/build/index.js.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f692e11f7e1538f4b91cc08b72f6f35d 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/build/toolDefinitions.js.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5311493213f23fd4bbe13c0facfe7058 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/build/types.js: -------------------------------------------------------------------------------- 1 | // MCP Server Types for Unity Integration 2 | export var SceneInfoDetail; 3 | (function (SceneInfoDetail) { 4 | SceneInfoDetail["RootObjectsOnly"] = "RootObjectsOnly"; 5 | SceneInfoDetail["FullHierarchy"] = "FullHierarchy"; 6 | })(SceneInfoDetail || (SceneInfoDetail = {})); 7 | export var GameObjectInfoDetail; 8 | (function (GameObjectInfoDetail) { 9 | GameObjectInfoDetail["BasicInfo"] = "BasicInfo"; 10 | GameObjectInfoDetail["IncludeComponents"] = "IncludeComponents"; 11 | GameObjectInfoDetail["IncludeChildren"] = "IncludeChildren"; 12 | GameObjectInfoDetail["IncludeComponentsAndChildren"] = "IncludeComponentsAndChildren"; 13 | })(GameObjectInfoDetail || (GameObjectInfoDetail = {})); 14 | -------------------------------------------------------------------------------- /mcpServer/build/types.js.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ccb598174a9e1944a98298a5e35a9df8 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/build/websocketHandler.js: -------------------------------------------------------------------------------- 1 | import { WebSocketServer, WebSocket } from 'ws'; 2 | export class WebSocketHandler { 3 | wsServer; // Add definite assignment assertion 4 | _port; // Make this a private field, not readonly 5 | unityConnection = null; 6 | editorState = { 7 | activeGameObjects: [], 8 | selectedObjects: [], 9 | playModeState: 'Stopped', 10 | sceneHierarchy: {} 11 | }; 12 | logBuffer = []; 13 | maxLogBufferSize = 1000; 14 | commandResultPromise = null; 15 | commandStartTime = null; 16 | lastHeartbeat = 0; 17 | connectionEstablished = false; 18 | pendingRequests = {}; 19 | constructor(port = 5010) { 20 | this._port = port; // Store in private field 21 | this.initializeWebSocketServer(port); 22 | } 23 | // Add a getter to expose port as readonly 24 | get port() { 25 | return this._port; 26 | } 27 | initializeWebSocketServer(port) { 28 | try { 29 | this.wsServer = new WebSocketServer({ port }); 30 | this.setupWebSocketServer(); 31 | console.error(`[Unity MCP] WebSocket server started on port ${this._port}`); 32 | } 33 | catch (error) { 34 | console.error(`[Unity MCP] ERROR starting WebSocket server on port ${port}:`, error); 35 | this.tryAlternativePort(port); 36 | } 37 | } 38 | tryAlternativePort(originalPort) { 39 | try { 40 | const alternativePort = originalPort + 1; 41 | console.error(`[Unity MCP] Trying alternative port ${alternativePort}...`); 42 | this._port = alternativePort; // Update the private field instead of readonly property 43 | this.wsServer = new WebSocketServer({ port: alternativePort }); 44 | this.setupWebSocketServer(); 45 | console.error(`[Unity MCP] WebSocket server started on alternative port ${this._port}`); 46 | } 47 | catch (secondError) { 48 | console.error(`[Unity MCP] FATAL: Could not start WebSocket server:`, secondError); 49 | throw new Error(`Failed to start WebSocket server: ${secondError}`); 50 | } 51 | } 52 | setupWebSocketServer() { 53 | console.error(`[Unity MCP] WebSocket server starting on port ${this._port}`); 54 | this.wsServer.on('listening', () => { 55 | console.error('[Unity MCP] WebSocket server is listening for connections'); 56 | }); 57 | this.wsServer.on('error', (error) => { 58 | console.error('[Unity MCP] WebSocket server error:', error); 59 | }); 60 | this.wsServer.on('connection', this.handleNewConnection.bind(this)); 61 | } 62 | handleNewConnection(ws) { 63 | console.error('[Unity MCP] Unity Editor connected'); 64 | this.unityConnection = ws; 65 | this.connectionEstablished = true; 66 | this.lastHeartbeat = Date.now(); 67 | // Send a simple handshake message to verify connection 68 | this.sendHandshake(); 69 | ws.on('message', (data) => this.handleIncomingMessage(data)); 70 | ws.on('error', (error) => { 71 | console.error('[Unity MCP] WebSocket error:', error); 72 | this.connectionEstablished = false; 73 | }); 74 | ws.on('close', () => { 75 | console.error('[Unity MCP] Unity Editor disconnected'); 76 | this.unityConnection = null; 77 | this.connectionEstablished = false; 78 | }); 79 | // Keep the automatic heartbeat for internal connection validation 80 | const pingInterval = setInterval(() => { 81 | if (ws.readyState === WebSocket.OPEN) { 82 | this.sendPing(); 83 | } 84 | else { 85 | clearInterval(pingInterval); 86 | } 87 | }, 30000); // Send heartbeat every 30 seconds 88 | } 89 | handleIncomingMessage(data) { 90 | try { 91 | // Update heartbeat on any message 92 | this.lastHeartbeat = Date.now(); 93 | const message = JSON.parse(data.toString()); 94 | console.error('[Unity MCP] Received message type:', message.type); 95 | this.handleUnityMessage(message); 96 | } 97 | catch (error) { 98 | console.error('[Unity MCP] Error handling message:', error); 99 | } 100 | } 101 | sendHandshake() { 102 | this.sendToUnity({ 103 | type: 'handshake', 104 | data: { message: 'MCP Server Connected' } 105 | }); 106 | } 107 | // Renamed from sendHeartbeat to sendPing for consistency with protocol 108 | sendPing() { 109 | this.sendToUnity({ 110 | type: "ping", 111 | data: { timestamp: Date.now() } 112 | }); 113 | } 114 | // Helper method to safely send messages to Unity 115 | sendToUnity(message) { 116 | try { 117 | if (this.unityConnection && this.unityConnection.readyState === WebSocket.OPEN) { 118 | this.unityConnection.send(JSON.stringify(message)); 119 | } 120 | } 121 | catch (error) { 122 | console.error(`[Unity MCP] Error sending message: ${error}`); 123 | this.connectionEstablished = false; 124 | } 125 | } 126 | handleUnityMessage(message) { 127 | switch (message.type) { 128 | case 'editorState': 129 | this.editorState = message.data; 130 | break; 131 | case 'commandResult': 132 | // Resolve the pending command result promise 133 | if (this.commandResultPromise) { 134 | this.commandResultPromise.resolve(message.data); 135 | this.commandResultPromise = null; 136 | this.commandStartTime = null; 137 | } 138 | break; 139 | case 'log': 140 | this.addLogEntry(message.data); 141 | break; 142 | case 'pong': 143 | // Update heartbeat reception timestamp when receiving pong 144 | this.lastHeartbeat = Date.now(); 145 | this.connectionEstablished = true; 146 | break; 147 | case 'sceneInfo': 148 | case 'gameObjectsDetails': 149 | this.handleRequestResponse(message); 150 | break; 151 | default: 152 | console.error('[Unity MCP] Unknown message type:', message); 153 | break; 154 | } 155 | } 156 | handleRequestResponse(message) { 157 | const requestId = message.data?.requestId; 158 | if (requestId && this.pendingRequests[requestId]) { 159 | // Fix the type issue by checking the property exists first 160 | if (this.pendingRequests[requestId]) { 161 | this.pendingRequests[requestId].resolve(message.data); 162 | delete this.pendingRequests[requestId]; 163 | } 164 | } 165 | } 166 | addLogEntry(logEntry) { 167 | // Add to buffer, removing oldest if at capacity 168 | this.logBuffer.push(logEntry); 169 | if (this.logBuffer.length > this.maxLogBufferSize) { 170 | this.logBuffer.shift(); 171 | } 172 | } 173 | async executeEditorCommand(code, timeoutMs = 5000) { 174 | if (!this.isConnected()) { 175 | throw new Error('Unity Editor is not connected'); 176 | } 177 | try { 178 | // Start timing the command execution 179 | this.commandStartTime = Date.now(); 180 | // Send the command to Unity 181 | this.sendToUnity({ 182 | type: 'executeEditorCommand', 183 | data: { code } 184 | }); 185 | // Wait for result with timeout 186 | return await Promise.race([ 187 | new Promise((resolve, reject) => { 188 | this.commandResultPromise = { resolve, reject }; 189 | }), 190 | new Promise((_, reject) => setTimeout(() => reject(new Error(`Command execution timed out after ${timeoutMs / 1000} seconds`)), timeoutMs)) 191 | ]); 192 | } 193 | catch (error) { 194 | // Reset command promise state if there's an error 195 | this.commandResultPromise = null; 196 | this.commandStartTime = null; 197 | throw error; 198 | } 199 | } 200 | // Return the current editor state - only used by tools, doesn't request updates 201 | getEditorState() { 202 | return this.editorState; 203 | } 204 | getLogEntries(options = {}) { 205 | const { types, count = 100, fields, messageContains, stackTraceContains, timestampAfter, timestampBefore } = options; 206 | // Apply all filters 207 | let filteredLogs = this.filterLogs(types, messageContains, stackTraceContains, timestampAfter, timestampBefore); 208 | // Apply count limit 209 | filteredLogs = filteredLogs.slice(-count); 210 | // Apply field selection if specified 211 | if (fields?.length) { 212 | return this.selectFields(filteredLogs, fields); 213 | } 214 | return filteredLogs; 215 | } 216 | filterLogs(types, messageContains, stackTraceContains, timestampAfter, timestampBefore) { 217 | return this.logBuffer.filter(log => { 218 | // Type filter 219 | if (types && !types.includes(log.logType)) 220 | return false; 221 | // Message content filter 222 | if (messageContains && !log.message.includes(messageContains)) 223 | return false; 224 | // Stack trace content filter 225 | if (stackTraceContains && !log.stackTrace.includes(stackTraceContains)) 226 | return false; 227 | // Timestamp filters 228 | if (timestampAfter && new Date(log.timestamp) < new Date(timestampAfter)) 229 | return false; 230 | if (timestampBefore && new Date(log.timestamp) > new Date(timestampBefore)) 231 | return false; 232 | return true; 233 | }); 234 | } 235 | selectFields(logs, fields) { 236 | return logs.map(log => { 237 | const selectedFields = {}; 238 | fields.forEach(field => { 239 | if (field in log) { 240 | selectedFields[field] = log[field]; 241 | } 242 | }); 243 | return selectedFields; 244 | }); 245 | } 246 | isConnected() { 247 | // More robust connection check 248 | if (this.unityConnection === null || this.unityConnection.readyState !== WebSocket.OPEN) { 249 | return false; 250 | } 251 | // Check if we've received messages from Unity recently 252 | if (!this.connectionEstablished) { 253 | return false; 254 | } 255 | // Check if we've received a heartbeat in the last 60 seconds 256 | const heartbeatTimeout = 60000; // 60 seconds 257 | if (Date.now() - this.lastHeartbeat > heartbeatTimeout) { 258 | console.error('[Unity MCP] Connection may be stale - no recent communication'); 259 | return false; 260 | } 261 | return true; 262 | } 263 | requestEditorState() { 264 | this.sendToUnity({ 265 | type: 'requestEditorState', 266 | data: {} 267 | }); 268 | } 269 | async requestSceneInfo(detailLevel) { 270 | return this.makeUnityRequest('getSceneInfo', { detailLevel }, 'sceneInfo'); 271 | } 272 | async requestGameObjectsInfo(instanceIDs, detailLevel) { 273 | return this.makeUnityRequest('getGameObjectsInfo', { instanceIDs, detailLevel }, 'gameObjectDetails'); 274 | } 275 | async makeUnityRequest(type, data, resultField) { 276 | if (!this.isConnected()) { 277 | throw new Error('Unity Editor is not connected'); 278 | } 279 | const requestId = crypto.randomUUID(); 280 | data.requestId = requestId; 281 | // Create a promise that will be resolved when we get the response 282 | const responsePromise = new Promise((resolve, reject) => { 283 | const timeout = setTimeout(() => { 284 | delete this.pendingRequests[requestId]; 285 | reject(new Error(`Request for ${type} timed out`)); 286 | }, 10000); // 10 second timeout 287 | this.pendingRequests[requestId] = { 288 | resolve: (data) => { 289 | clearTimeout(timeout); 290 | resolve(data[resultField]); 291 | }, 292 | reject, 293 | type 294 | }; 295 | }); 296 | // Send the request to Unity 297 | this.sendToUnity({ 298 | type, 299 | data 300 | }); 301 | return responsePromise; 302 | } 303 | // Support for file system tools by adding a method to send generic messages 304 | async sendMessage(message) { 305 | if (this.unityConnection && this.unityConnection.readyState === WebSocket.OPEN) { 306 | const messageStr = typeof message === 'string' ? message : JSON.stringify(message); 307 | return new Promise((resolve, reject) => { 308 | this.unityConnection.send(messageStr, (err) => { 309 | if (err) { 310 | reject(err); 311 | } 312 | else { 313 | resolve(); 314 | } 315 | }); 316 | }); 317 | } 318 | return Promise.resolve(); 319 | } 320 | async close() { 321 | if (this.unityConnection) { 322 | try { 323 | this.unityConnection.close(); 324 | } 325 | catch (error) { 326 | console.error('[Unity MCP] Error closing Unity connection:', error); 327 | } 328 | this.unityConnection = null; 329 | } 330 | return new Promise((resolve) => { 331 | try { 332 | this.wsServer.close(() => { 333 | console.error('[Unity MCP] WebSocket server closed'); 334 | resolve(); 335 | }); 336 | } 337 | catch (error) { 338 | console.error('[Unity MCP] Error closing WebSocket server:', error); 339 | resolve(); // Resolve anyway to allow the process to exit 340 | } 341 | }); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /mcpServer/build/websocketHandler.js.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cb7f47c2a9b36224ea430eef4be8c425 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | unity-mcp-server: 5 | build: . 6 | ports: 7 | - "${MCP_WEBSOCKET_PORT:-5010}:${MCP_WEBSOCKET_PORT:-5010}" 8 | environment: 9 | - MCP_WEBSOCKET_PORT=${MCP_WEBSOCKET_PORT:-5010} 10 | restart: unless-stopped 11 | volumes: 12 | # Optional volume for persisting data if needed 13 | - ./logs:/app/logs 14 | -------------------------------------------------------------------------------- /mcpServer/docker-compose.yml.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8368306bf6b173448bd61e24127aa30a 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/node_modules.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ef0f97b30ee11c140bfe700d53734ed7 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /mcpServer/package-lock.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 24da956a0f095f048a9cf055f38c5a2b 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unity-mcp-server", 3 | "version": "0.1.0", 4 | "description": "MCP server for Unity integration", 5 | "type": "module", 6 | "bin": { 7 | "unity-mcp-server": "./build/index.js" 8 | }, 9 | "files": [ 10 | "build" 11 | ], 12 | "scripts": { 13 | "build": "tsc", 14 | "start": "node build/index.js", 15 | "dev": "tsc --watch", 16 | "inspector": "npx @modelcontextprotocol/inspector build/index.js" 17 | }, 18 | "dependencies": { 19 | "@modelcontextprotocol/sdk": "1.7.0", 20 | "ws": "^8.16.0", 21 | "diff": "^5.1.0", 22 | "minimatch": "^9.0.3", 23 | "zod": "^3.22.4", 24 | "zod-to-json-schema": "^3.22.1" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^20.11.24", 28 | "@types/ws": "^8.5.10", 29 | "@types/diff": "^5.0.8", 30 | "@types/minimatch": "^5.1.2", 31 | "typescript": "^5.3.3" 32 | } 33 | } -------------------------------------------------------------------------------- /mcpServer/package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 857dd34916bc4c349a72631f489bd4fb 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/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 | -------------------------------------------------------------------------------- /mcpServer/src.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f627733ca99f539468bafb6a9816d43e 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /mcpServer/src/filesystemTools.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 2 | import { WebSocketHandler } from './websocketHandler.js'; 3 | import fs from 'fs/promises'; 4 | import { Dirent } from 'fs'; // Import Dirent instead of DirEnt 5 | import path from 'path'; 6 | import { createTwoFilesPatch } from 'diff'; 7 | import { minimatch } from 'minimatch'; 8 | import { 9 | ReadFileArgsSchema, 10 | ReadMultipleFilesArgsSchema, 11 | WriteFileArgsSchema, 12 | EditFileArgsSchema, 13 | ListDirectoryArgsSchema, 14 | DirectoryTreeArgsSchema, 15 | SearchFilesArgsSchema, 16 | GetFileInfoArgsSchema, 17 | FindAssetsByTypeArgsSchema 18 | } from './toolDefinitions.js'; 19 | 20 | // Interface definitions 21 | interface FileInfo { 22 | size: number; 23 | created: Date; 24 | modified: Date; 25 | accessed: Date; 26 | isDirectory: boolean; 27 | isFile: boolean; 28 | permissions: string; 29 | } 30 | 31 | interface TreeEntry { 32 | name: string; 33 | type: 'file' | 'directory'; 34 | children?: TreeEntry[]; 35 | } 36 | 37 | // Helper functions 38 | async function validatePath(requestedPath: string, assetRootPath: string): Promise { 39 | // If path is empty or just quotes, use the asset root path directly 40 | if (!requestedPath || requestedPath.trim() === '' || requestedPath.trim() === '""' || requestedPath.trim() === "''") { 41 | console.error(`[Unity MCP] Using asset root path: ${assetRootPath}`); 42 | return assetRootPath; 43 | } 44 | 45 | // Clean the path to remove any unexpected quotes or escape characters 46 | let cleanPath = requestedPath.replace(/['"\\]/g, ''); 47 | 48 | // Handle empty path after cleaning 49 | if (!cleanPath || cleanPath.trim() === '') { 50 | return assetRootPath; 51 | } 52 | 53 | // Normalize path to handle both Windows and Unix-style paths 54 | const normalized = path.normalize(cleanPath); 55 | 56 | // Resolve the path (absolute or relative) 57 | let absolute = resolvePathToAssetRoot(normalized, assetRootPath); 58 | const resolvedPath = path.resolve(absolute); 59 | 60 | // Ensure we don't escape out of the Unity project folder 61 | validatePathSecurity(resolvedPath, assetRootPath, requestedPath); 62 | 63 | return resolvedPath; 64 | } 65 | 66 | function resolvePathToAssetRoot(pathToResolve: string, assetRootPath: string): string { 67 | if (path.isAbsolute(pathToResolve)) { 68 | console.error(`[Unity MCP] Absolute path requested: ${pathToResolve}`); 69 | 70 | // If the absolute path is outside the project, try alternative resolutions 71 | if (!pathToResolve.startsWith(assetRootPath)) { 72 | // Try 1: Treat as relative path 73 | const tryRelative = path.join(assetRootPath, pathToResolve); 74 | try { 75 | fs.access(tryRelative); 76 | console.error(`[Unity MCP] Treating as relative path: ${tryRelative}`); 77 | return tryRelative; 78 | } catch { 79 | // Try 2: Try to extract path relative to Assets if it contains "Assets" 80 | if (pathToResolve.includes('Assets')) { 81 | const assetsIndex = pathToResolve.indexOf('Assets'); 82 | const relativePath = pathToResolve.substring(assetsIndex + 7); // +7 to skip "Assets/" 83 | const newPath = path.join(assetRootPath, relativePath); 84 | console.error(`[Unity MCP] Trying via Assets path: ${newPath}`); 85 | 86 | try { 87 | fs.access(newPath); 88 | return newPath; 89 | } catch { /* Use original if all else fails */ } 90 | } 91 | } 92 | } 93 | return pathToResolve; 94 | } else { 95 | // For relative paths, join with asset root path 96 | return path.join(assetRootPath, pathToResolve); 97 | } 98 | } 99 | 100 | function validatePathSecurity(resolvedPath: string, assetRootPath: string, requestedPath: string): void { 101 | if (!resolvedPath.startsWith(assetRootPath) && requestedPath.trim() !== '') { 102 | console.error(`[Unity MCP] Access denied: Path ${requestedPath} is outside the project directory`); 103 | console.error(`[Unity MCP] Resolved to: ${resolvedPath}`); 104 | console.error(`[Unity MCP] Expected to be within: ${assetRootPath}`); 105 | throw new Error(`Access denied: Path ${requestedPath} is outside the Unity project directory`); 106 | } 107 | } 108 | 109 | async function getFileStats(filePath: string): Promise { 110 | const stats = await fs.stat(filePath); 111 | return { 112 | size: stats.size, 113 | created: stats.birthtime, 114 | modified: stats.mtime, 115 | accessed: stats.atime, 116 | isDirectory: stats.isDirectory(), 117 | isFile: stats.isFile(), 118 | permissions: stats.mode.toString(8).slice(-3), 119 | }; 120 | } 121 | 122 | async function searchFiles( 123 | rootPath: string, 124 | pattern: string, 125 | excludePatterns: string[] = [] 126 | ): Promise { 127 | const results: string[] = []; 128 | 129 | async function search(currentPath: string) { 130 | const entries = await fs.readdir(currentPath, { withFileTypes: true }); 131 | 132 | for (const entry of entries) { 133 | const fullPath = path.join(currentPath, entry.name); 134 | 135 | try { 136 | // Check if path matches any exclude pattern 137 | const relativePath = path.relative(rootPath, fullPath); 138 | if (isPathExcluded(relativePath, excludePatterns)) continue; 139 | 140 | if (entry.name.toLowerCase().includes(pattern.toLowerCase())) { 141 | results.push(fullPath); 142 | } 143 | 144 | if (entry.isDirectory()) { 145 | await search(fullPath); 146 | } 147 | } catch (error) { 148 | // Skip invalid paths during search 149 | continue; 150 | } 151 | } 152 | } 153 | 154 | await search(rootPath); 155 | return results; 156 | } 157 | 158 | function isPathExcluded(relativePath: string, excludePatterns: string[]): boolean { 159 | return excludePatterns.some(pattern => { 160 | const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`; 161 | return minimatch(relativePath, globPattern, { dot: true }); 162 | }); 163 | } 164 | 165 | function normalizeLineEndings(text: string): string { 166 | return text.replace(/\r\n/g, '\n'); 167 | } 168 | 169 | function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string { 170 | // Ensure consistent line endings for diff 171 | const normalizedOriginal = normalizeLineEndings(originalContent); 172 | const normalizedNew = normalizeLineEndings(newContent); 173 | 174 | return createTwoFilesPatch( 175 | filepath, 176 | filepath, 177 | normalizedOriginal, 178 | normalizedNew, 179 | 'original', 180 | 'modified' 181 | ); 182 | } 183 | 184 | async function applyFileEdits( 185 | filePath: string, 186 | edits: Array<{oldText: string, newText: string}>, 187 | dryRun = false 188 | ): Promise { 189 | // Read file content and normalize line endings 190 | const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8')); 191 | 192 | // Apply edits sequentially 193 | let modifiedContent = content; 194 | for (const edit of edits) { 195 | const normalizedOld = normalizeLineEndings(edit.oldText); 196 | const normalizedNew = normalizeLineEndings(edit.newText); 197 | 198 | // If exact match exists, use it 199 | if (modifiedContent.includes(normalizedOld)) { 200 | modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew); 201 | continue; 202 | } 203 | 204 | // Try line-by-line matching with whitespace flexibility 205 | modifiedContent = applyFlexibleLineEdit(modifiedContent, normalizedOld, normalizedNew); 206 | } 207 | 208 | // Create unified diff 209 | const diff = createUnifiedDiff(content, modifiedContent, filePath); 210 | const formattedDiff = formatDiff(diff); 211 | 212 | if (!dryRun) { 213 | await fs.writeFile(filePath, modifiedContent, 'utf-8'); 214 | } 215 | 216 | return formattedDiff; 217 | } 218 | 219 | function applyFlexibleLineEdit(content: string, oldText: string, newText: string): string { 220 | const oldLines = oldText.split('\n'); 221 | const contentLines = content.split('\n'); 222 | 223 | for (let i = 0; i <= contentLines.length - oldLines.length; i++) { 224 | const potentialMatch = contentLines.slice(i, i + oldLines.length); 225 | 226 | // Compare lines with normalized whitespace 227 | const isMatch = oldLines.every((oldLine, j) => { 228 | const contentLine = potentialMatch[j]; 229 | return oldLine.trim() === contentLine.trim(); 230 | }); 231 | 232 | if (isMatch) { 233 | // Preserve indentation 234 | const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''; 235 | const newLines = newText.split('\n').map((line, j) => { 236 | if (j === 0) return originalIndent + line.trimStart(); 237 | 238 | const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || ''; 239 | const newIndent = line.match(/^\s*/)?.[0] || ''; 240 | 241 | if (oldIndent && newIndent) { 242 | const relativeIndent = newIndent.length - oldIndent.length; 243 | return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart(); 244 | } 245 | return line; 246 | }); 247 | 248 | contentLines.splice(i, oldLines.length, ...newLines); 249 | return contentLines.join('\n'); 250 | } 251 | } 252 | 253 | throw new Error(`Could not find exact match for edit:\n${oldText}`); 254 | } 255 | 256 | function formatDiff(diff: string): string { 257 | let numBackticks = 3; 258 | while (diff.includes('`'.repeat(numBackticks))) { 259 | numBackticks++; 260 | } 261 | return `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`; 262 | } 263 | 264 | async function buildDirectoryTree(currentPath: string, assetRootPath: string, maxDepth: number = 5, currentDepth: number = 0): Promise { 265 | if (currentDepth >= maxDepth) { 266 | return [{ name: "...", type: "directory" }]; 267 | } 268 | 269 | const validPath = await validatePath(currentPath, assetRootPath); 270 | const entries = await fs.readdir(validPath, { withFileTypes: true }); 271 | const result: TreeEntry[] = []; 272 | 273 | for (const entry of entries) { 274 | const entryData: TreeEntry = { 275 | name: entry.name, 276 | type: entry.isDirectory() ? 'directory' : 'file' 277 | }; 278 | 279 | if (entry.isDirectory()) { 280 | const subPath = path.join(currentPath, entry.name); 281 | entryData.children = await buildDirectoryTree(subPath, assetRootPath, maxDepth, currentDepth + 1); 282 | } 283 | 284 | result.push(entryData); 285 | } 286 | 287 | return result; 288 | } 289 | 290 | // Function to recognize Unity asset types based on file extension 291 | function getUnityAssetType(filePath: string): string { 292 | const ext = path.extname(filePath).toLowerCase(); 293 | 294 | // Common Unity asset types 295 | const assetTypes: Record = { 296 | '.unity': 'Scene', 297 | '.prefab': 'Prefab', 298 | '.mat': 'Material', 299 | '.fbx': 'Model', 300 | '.cs': 'Script', 301 | '.anim': 'Animation', 302 | '.controller': 'Animator Controller', 303 | '.asset': 'ScriptableObject', 304 | '.png': 'Texture', 305 | '.jpg': 'Texture', 306 | '.jpeg': 'Texture', 307 | '.tga': 'Texture', 308 | '.wav': 'Audio', 309 | '.mp3': 'Audio', 310 | '.ogg': 'Audio', 311 | '.shader': 'Shader', 312 | '.compute': 'Compute Shader', 313 | '.ttf': 'Font', 314 | '.otf': 'Font', 315 | '.physicMaterial': 'Physics Material', 316 | '.mask': 'Avatar Mask', 317 | '.playable': 'Playable', 318 | '.mixer': 'Audio Mixer', 319 | '.renderTexture': 'Render Texture', 320 | '.lighting': 'Lighting Settings', 321 | '.shadervariants': 'Shader Variants', 322 | '.spriteatlas': 'Sprite Atlas', 323 | '.guiskin': 'GUI Skin', 324 | '.flare': 'Flare', 325 | '.brush': 'Brush', 326 | '.overrideController': 'Animator Override Controller', 327 | '.preset': 'Preset', 328 | '.terrainlayer': 'Terrain Layer', 329 | '.signal': 'Signal', 330 | '.signalasset': 'Signal Asset', 331 | '.giparams': 'Global Illumination Parameters', 332 | '.cubemap': 'Cubemap', 333 | }; 334 | 335 | return assetTypes[ext] || 'Other'; 336 | } 337 | 338 | // Get file extensions for Unity asset types 339 | function getFileExtensionsForType(type: string): string[] { 340 | type = type.toLowerCase(); 341 | const extensionMap: Record = { 342 | 'scene': ['.unity'], 343 | 'prefab': ['.prefab'], 344 | 'material': ['.mat'], 345 | 'script': ['.cs'], 346 | 'model': ['.fbx', '.obj', '.blend', '.max', '.mb', '.ma'], 347 | 'texture': ['.png', '.jpg', '.jpeg', '.tga', '.tif', '.tiff', '.psd', '.exr', '.hdr'], 348 | 'audio': ['.wav', '.mp3', '.ogg', '.aiff', '.aif'], 349 | 'animation': ['.anim'], 350 | 'animator': ['.controller'], 351 | 'shader': ['.shader', '.compute', '.cginc'] 352 | }; 353 | 354 | return extensionMap[type] || []; 355 | } 356 | 357 | // Handler function to process filesystem tools 358 | export async function handleFilesystemTool(name: string, args: any, projectPath: string) { 359 | try { 360 | switch (name) { 361 | case "read_file": { 362 | const parsed = ReadFileArgsSchema.safeParse(args); 363 | if (!parsed.success) return invalidArgsResponse(parsed.error); 364 | 365 | const validPath = await validatePath(parsed.data.path, projectPath); 366 | const content = await fs.readFile(validPath, "utf-8"); 367 | return { content: [{ type: "text", text: content }] }; 368 | } 369 | 370 | case "read_multiple_files": { 371 | const parsed = ReadMultipleFilesArgsSchema.safeParse(args); 372 | if (!parsed.success) return invalidArgsResponse(parsed.error); 373 | 374 | const results = await Promise.all( 375 | parsed.data.paths.map(async (filePath: string) => { 376 | try { 377 | const validPath = await validatePath(filePath, projectPath); 378 | const content = await fs.readFile(validPath, "utf-8"); 379 | return `${filePath}:\n${content}\n`; 380 | } catch (error) { 381 | return `${filePath}: Error - ${getErrorMessage(error)}`; 382 | } 383 | }), 384 | ); 385 | return { content: [{ type: "text", text: results.join("\n---\n") }] }; 386 | } 387 | 388 | case "write_file": { 389 | const parsed = WriteFileArgsSchema.safeParse(args); 390 | if (!parsed.success) return invalidArgsResponse(parsed.error); 391 | 392 | const validPath = await validatePath(parsed.data.path, projectPath); 393 | 394 | // Ensure directory exists 395 | const dirPath = path.dirname(validPath); 396 | await fs.mkdir(dirPath, { recursive: true }); 397 | 398 | await fs.writeFile(validPath, parsed.data.content, "utf-8"); 399 | return { 400 | content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }] 401 | }; 402 | } 403 | 404 | case "edit_file": { 405 | const parsed = EditFileArgsSchema.safeParse(args); 406 | if (!parsed.success) return invalidArgsResponse(parsed.error); 407 | 408 | const validPath = await validatePath(parsed.data.path, projectPath); 409 | const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun); 410 | return { content: [{ type: "text", text: result }] }; 411 | } 412 | 413 | case "list_directory": { 414 | const parsed = ListDirectoryArgsSchema.safeParse(args); 415 | if (!parsed.success) return invalidArgsResponse(parsed.error); 416 | 417 | const validPath = await validatePath(parsed.data.path, projectPath); 418 | const entries = await fs.readdir(validPath, { withFileTypes: true }); 419 | const formatted = entries 420 | .map((entry) => formatDirectoryEntry(entry, validPath)) 421 | .join("\n"); 422 | return { content: [{ type: "text", text: formatted }] }; 423 | } 424 | 425 | case "directory_tree": { 426 | const parsed = DirectoryTreeArgsSchema.safeParse(args); 427 | if (!parsed.success) return invalidArgsResponse(parsed.error); 428 | 429 | const treeData = await buildDirectoryTree(parsed.data.path, projectPath, parsed.data.maxDepth); 430 | return { content: [{ type: "text", text: JSON.stringify(treeData, null, 2) }] }; 431 | } 432 | 433 | case "search_files": { 434 | const parsed = SearchFilesArgsSchema.safeParse(args); 435 | if (!parsed.success) return invalidArgsResponse(parsed.error); 436 | 437 | const validPath = await validatePath(parsed.data.path, projectPath); 438 | const results = await searchFiles(validPath, parsed.data.pattern, parsed.data.excludePatterns); 439 | return { 440 | content: [{ 441 | type: "text", 442 | text: results.length > 0 443 | ? `Found ${results.length} results:\n${results.join("\n")}` 444 | : "No matches found" 445 | }] 446 | }; 447 | } 448 | 449 | case "get_file_info": { 450 | const parsed = GetFileInfoArgsSchema.safeParse(args); 451 | if (!parsed.success) return invalidArgsResponse(parsed.error); 452 | 453 | const validPath = await validatePath(parsed.data.path, projectPath); 454 | const info = await getFileStats(validPath); 455 | 456 | // Add Unity-specific info if it's an asset file 457 | const additionalInfo: Record = {}; 458 | if (info.isFile) { 459 | additionalInfo.assetType = getUnityAssetType(validPath); 460 | } 461 | 462 | const formattedInfo = Object.entries({ ...info, ...additionalInfo }) 463 | .map(([key, value]) => `${key}: ${value}`) 464 | .join("\n"); 465 | 466 | return { content: [{ type: "text", text: formattedInfo }] }; 467 | } 468 | 469 | case "find_assets_by_type": { 470 | const parsed = FindAssetsByTypeArgsSchema.safeParse(args); 471 | if (!parsed.success) return invalidArgsResponse(parsed.error); 472 | 473 | const assetType = parsed.data.assetType.replace(/['"]/g, ''); 474 | const searchPath = parsed.data.searchPath.replace(/['"]/g, ''); 475 | const maxDepth = parsed.data.maxDepth; 476 | 477 | console.error(`[Unity MCP] Finding assets of type "${assetType}" in path "${searchPath}" with maxDepth ${maxDepth}`); 478 | 479 | const validPath = await validatePath(searchPath, projectPath); 480 | const results = await findAssetsByType(assetType, validPath, maxDepth, projectPath); 481 | 482 | return { 483 | content: [{ 484 | type: "text", 485 | text: results.length > 0 486 | ? `Found ${results.length} ${assetType} assets:\n${JSON.stringify(results, null, 2)}` 487 | : `No "${assetType}" assets found in ${searchPath || "Assets"}` 488 | }] 489 | }; 490 | } 491 | 492 | default: 493 | return { 494 | content: [{ type: "text", text: `Unknown tool: ${name}` }], 495 | isError: true, 496 | }; 497 | } 498 | } catch (error) { 499 | return { 500 | content: [{ type: "text", text: `Error: ${getErrorMessage(error)}` }], 501 | isError: true, 502 | }; 503 | } 504 | } 505 | 506 | function getErrorMessage(error: unknown): string { 507 | return error instanceof Error ? error.message : String(error); 508 | } 509 | 510 | function invalidArgsResponse(error: any) { 511 | return { 512 | content: [{ type: "text", text: `Invalid arguments: ${error}` }], 513 | isError: true 514 | }; 515 | } 516 | 517 | // Fixed function to use proper Dirent type 518 | function formatDirectoryEntry(entry: Dirent, basePath: string): string { 519 | if (entry.isDirectory()) { 520 | return `[DIR] ${entry.name}`; 521 | } else { 522 | // For files, detect Unity asset type 523 | const filePath = path.join(basePath, entry.name); 524 | const assetType = getUnityAssetType(filePath); 525 | return `[${assetType}] ${entry.name}`; 526 | } 527 | } 528 | 529 | async function findAssetsByType( 530 | assetType: string, 531 | searchPath: string, 532 | maxDepth: number, 533 | projectPath: string 534 | ): Promise> { 535 | const results: Array<{path: string, name: string, type: string}> = []; 536 | const extensions = getFileExtensionsForType(assetType); 537 | 538 | async function searchAssets(dir: string, currentDepth: number = 1) { 539 | // Stop recursion if we've reached the maximum depth 540 | if (maxDepth !== -1 && currentDepth > maxDepth) { 541 | return; 542 | } 543 | 544 | try { 545 | const entries = await fs.readdir(dir, { withFileTypes: true }); 546 | 547 | for (const entry of entries) { 548 | const fullPath = path.join(dir, entry.name); 549 | const relativePath = path.relative(projectPath, fullPath); 550 | 551 | if (entry.isDirectory()) { 552 | // Recursively search subdirectories 553 | await searchAssets(fullPath, currentDepth + 1); 554 | } else { 555 | // Check if the file matches the requested asset type 556 | const ext = path.extname(entry.name).toLowerCase(); 557 | 558 | if (extensions.length === 0) { 559 | // If no extensions specified, match by Unity asset type 560 | const fileAssetType = getUnityAssetType(fullPath); 561 | if (fileAssetType.toLowerCase() === assetType.toLowerCase()) { 562 | results.push({ 563 | path: relativePath, 564 | name: entry.name, 565 | type: fileAssetType 566 | }); 567 | } 568 | } else if (extensions.includes(ext)) { 569 | // Match by extension 570 | results.push({ 571 | path: relativePath, 572 | name: entry.name, 573 | type: assetType 574 | }); 575 | } 576 | } 577 | } 578 | } catch (error) { 579 | console.error(`Error accessing directory ${dir}:`, error); 580 | } 581 | } 582 | 583 | await searchAssets(searchPath); 584 | return results; 585 | } 586 | 587 | // This function is deprecated and now just a stub 588 | export function registerFilesystemTools(server: Server, wsHandler: WebSocketHandler) { 589 | console.log("Filesystem tools are now registered in toolDefinitions.ts"); 590 | } 591 | -------------------------------------------------------------------------------- /mcpServer/src/filesystemTools.ts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0879cebd8d062b0499f111626e8de524 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/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 { WebSocketHandler } from './websocketHandler.js'; 5 | import { registerTools } from './toolDefinitions.js'; 6 | import path from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | import fs from 'fs'; 9 | 10 | class UnityMCPServer { 11 | private server: Server; 12 | private wsHandler: WebSocketHandler; 13 | 14 | constructor() { 15 | // Initialize MCP Server 16 | this.server = new Server( 17 | { name: 'unity-mcp-server', version: '0.2.0' }, 18 | { capabilities: { tools: {} } } 19 | ); 20 | 21 | // Setup project paths and websocket 22 | const wsPort = parseInt(process.env.MCP_WEBSOCKET_PORT || '5010'); 23 | const projectRootPath = this.setupProjectPaths(); 24 | 25 | // Initialize WebSocket Handler for Unity communication 26 | this.wsHandler = new WebSocketHandler(wsPort); 27 | 28 | // Register MCP tools 29 | registerTools(this.server, this.wsHandler); 30 | 31 | // Error handling 32 | this.server.onerror = (error) => console.error('[MCP Error]', error); 33 | this.setupShutdownHandlers(); 34 | } 35 | 36 | private setupProjectPaths(): string { 37 | const __filename = fileURLToPath(import.meta.url); 38 | const __dirname = path.dirname(__filename); 39 | console.error(`[Unity MCP] Server starting from directory: ${__dirname}`); 40 | 41 | // Get the project root path (parent of Assets) 42 | let projectRootPath = process.env.UNITY_PROJECT_PATH || this.determineUnityProjectPath(__dirname); 43 | projectRootPath = path.normalize(projectRootPath.replace(/["']/g, '')); 44 | 45 | // Make sure path ends with a directory separator 46 | if (!projectRootPath.endsWith(path.sep)) { 47 | projectRootPath += path.sep; 48 | } 49 | 50 | // Create the full path to the Assets folder 51 | const projectPath = path.join(projectRootPath, 'Assets') + path.sep; 52 | this.setupEnvironmentPath(projectRootPath, projectPath); 53 | 54 | return projectRootPath; 55 | } 56 | 57 | private setupEnvironmentPath(projectRootPath: string, projectPath: string): void { 58 | try { 59 | if (fs.existsSync(projectPath)) { 60 | console.error(`[Unity MCP] Using project path: ${projectPath}`); 61 | process.env.UNITY_PROJECT_PATH = projectPath; 62 | } else { 63 | console.error(`[Unity MCP] WARNING: Assets folder not found at ${projectPath}`); 64 | console.error(`[Unity MCP] Using project root instead: ${projectRootPath}`); 65 | process.env.UNITY_PROJECT_PATH = projectRootPath; 66 | } 67 | } catch (error) { 68 | console.error(`[Unity MCP] Error checking project path: ${error}`); 69 | process.env.UNITY_PROJECT_PATH = process.cwd(); 70 | } 71 | } 72 | 73 | private setupShutdownHandlers(): void { 74 | const cleanupHandler = async () => { 75 | await this.cleanup(); 76 | process.exit(0); 77 | }; 78 | 79 | process.on('SIGINT', cleanupHandler); 80 | process.on('SIGTERM', cleanupHandler); 81 | } 82 | 83 | /** 84 | * Determine the Unity project path based on the script location 85 | */ 86 | private determineUnityProjectPath(scriptDir: string): string { 87 | scriptDir = path.normalize(scriptDir); 88 | console.error(`[Unity MCP] Script directory: ${scriptDir}`); 89 | 90 | // Case 1: Installed in Assets folder 91 | const assetsMatch = /^(.+?[\/\\]Assets)[\/\\].*$/i.exec(scriptDir); 92 | if (assetsMatch) { 93 | const projectRoot = path.dirname(assetsMatch[1]); 94 | console.error(`[Unity MCP] Detected installation in Assets folder: ${projectRoot}`); 95 | return projectRoot; 96 | } 97 | 98 | // Case 2: Installed via Package Manager 99 | const libraryMatch = /^(.+?[\/\\]Library)[\/\\]PackageCache[\/\\].*$/i.exec(scriptDir); 100 | if (libraryMatch) { 101 | const projectRoot = path.dirname(libraryMatch[1]); 102 | console.error(`[Unity MCP] Detected installation via Package Manager: ${projectRoot}`); 103 | 104 | const assetsPath = path.join(projectRoot, 'Assets'); 105 | if (fs.existsSync(assetsPath)) { 106 | return projectRoot; 107 | } 108 | } 109 | 110 | // Case 3: Check parent directories 111 | for (const dir of this.getParentDirectories(scriptDir)) { 112 | // Check if this directory is "UnityMCP" 113 | if (path.basename(dir) === 'UnityMCP') { 114 | console.error(`[Unity MCP] Found UnityMCP directory at: ${dir}`); 115 | return dir; 116 | } 117 | 118 | // Check if this directory contains an Assets folder 119 | const assetsDir = path.join(dir, 'Assets'); 120 | try { 121 | if (fs.existsSync(assetsDir) && fs.statSync(assetsDir).isDirectory()) { 122 | console.error(`[Unity MCP] Found Unity project at: ${dir}`); 123 | return dir; 124 | } 125 | } catch (e) { 126 | // Ignore errors checking directories 127 | } 128 | } 129 | 130 | // Fallback 131 | console.error('[Unity MCP] Could not detect Unity project directory. Using current directory.'); 132 | return process.cwd(); 133 | } 134 | 135 | private getParentDirectories(filePath: string): string[] { 136 | const result: string[] = []; 137 | const dirs = filePath.split(path.sep); 138 | 139 | for (let i = 1; i <= dirs.length; i++) { 140 | result.push(dirs.slice(0, i).join(path.sep)); 141 | } 142 | 143 | return result; 144 | } 145 | 146 | private async cleanup() { 147 | console.error('Cleaning up resources...'); 148 | await this.wsHandler.close(); 149 | await this.server.close(); 150 | } 151 | 152 | async run() { 153 | const transport = new StdioServerTransport(); 154 | await this.server.connect(transport); 155 | console.error('[Unity MCP] Server running and ready to accept connections'); 156 | console.error('[Unity MCP] WebSocket server listening on port', this.wsHandler.port); 157 | } 158 | } 159 | 160 | // Start the server 161 | const server = new UnityMCPServer(); 162 | server.run().catch(err => { 163 | console.error('Fatal error in MCP server:', err); 164 | process.exit(1); 165 | }); -------------------------------------------------------------------------------- /mcpServer/src/index.ts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d7ce6614f5ba48a4a9504c095cd4cd66 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/src/toolDefinitions.ts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9b77adee6d1696245a4a6632a07e8c71 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/src/types.ts: -------------------------------------------------------------------------------- 1 | // MCP Server Types for Unity Integration 2 | 3 | // Unity Editor State representation 4 | export interface UnityEditorState { 5 | activeGameObjects: any[]; 6 | selectedObjects: any[]; 7 | playModeState: string; 8 | sceneHierarchy: any; 9 | projectName?: string; 10 | unityVersion?: string; 11 | renderPipeline?: string; 12 | buildTarget?: string; 13 | graphicsDeviceType?: string; 14 | currentSceneName?: string; 15 | currentScenePath?: string; 16 | timestamp?: string; 17 | availableMenuItems?: string[]; 18 | } 19 | 20 | // Log entry from Unity 21 | export interface LogEntry { 22 | message: string; 23 | stackTrace: string; 24 | logType: string; 25 | timestamp: string; 26 | } 27 | 28 | // Scene info from Unity 29 | export interface SceneInfoMessage { 30 | type: 'sceneInfo'; 31 | data: { 32 | requestId: string; 33 | sceneInfo: any; 34 | timestamp: string; 35 | }; 36 | } 37 | 38 | // Game objects details from Unity 39 | export interface GameObjectsDetailsMessage { 40 | type: 'gameObjectsDetails'; 41 | data: { 42 | requestId: string; 43 | gameObjectDetails: any[]; 44 | count: number; 45 | timestamp: string; 46 | }; 47 | } 48 | 49 | // Message types from Unity to Server 50 | export interface EditorStateMessage { 51 | type: 'editorState'; 52 | data: UnityEditorState; 53 | } 54 | 55 | export interface CommandResultMessage { 56 | type: 'commandResult'; 57 | data: any; 58 | } 59 | 60 | export interface LogMessage { 61 | type: 'log'; 62 | data: LogEntry; 63 | } 64 | 65 | export interface PongMessage { 66 | type: 'pong'; 67 | data: { timestamp: number }; 68 | } 69 | 70 | // Message types from Server to Unity 71 | export interface ExecuteEditorCommandMessage { 72 | type: 'executeEditorCommand'; 73 | data: { 74 | code: string; 75 | }; 76 | } 77 | 78 | export interface HandshakeMessage { 79 | type: 'handshake'; 80 | data: { message: string }; 81 | } 82 | 83 | export interface PingMessage { 84 | type: 'ping'; 85 | data: { timestamp: number }; 86 | } 87 | 88 | export interface RequestEditorStateMessage { 89 | type: 'requestEditorState'; 90 | data: Record; 91 | } 92 | 93 | export interface GetSceneInfoMessage { 94 | type: 'getSceneInfo'; 95 | data: { 96 | requestId: string; 97 | detailLevel: string; 98 | }; 99 | } 100 | 101 | export interface GetGameObjectsInfoMessage { 102 | type: 'getGameObjectsInfo'; 103 | data: { 104 | requestId: string; 105 | instanceIDs: number[]; 106 | detailLevel: string; 107 | }; 108 | } 109 | 110 | // Union type for all Unity messages 111 | export type UnityMessage = 112 | | EditorStateMessage 113 | | CommandResultMessage 114 | | LogMessage 115 | | PongMessage 116 | | SceneInfoMessage 117 | | GameObjectsDetailsMessage; 118 | 119 | // Union type for all Server messages 120 | export type ServerMessage = 121 | | ExecuteEditorCommandMessage 122 | | HandshakeMessage 123 | | PingMessage 124 | | RequestEditorStateMessage 125 | | GetSceneInfoMessage 126 | | GetGameObjectsInfoMessage; 127 | 128 | // Command result handling 129 | export interface CommandPromise { 130 | resolve: (data?: any) => void; 131 | reject: (reason?: any) => void; 132 | } 133 | 134 | export interface TreeEntry { 135 | name: string; 136 | type: 'file' | 'directory'; 137 | children?: TreeEntry[]; 138 | } 139 | 140 | export interface MCPSceneInfo { 141 | name: string; 142 | path: string; 143 | rootGameObjects: any[]; 144 | buildIndex: number; 145 | isDirty: boolean; 146 | isLoaded: boolean; 147 | } 148 | 149 | export interface MCPTransformInfo { 150 | position: { x: number, y: number, z: number }; 151 | rotation: { x: number, y: number, z: number }; 152 | localPosition: { x: number, y: number, z: number }; 153 | localRotation: { x: number, y: number, z: number }; 154 | localScale: { x: number, y: number, z: number }; 155 | } 156 | 157 | export interface MCPComponentInfo { 158 | type: string; 159 | isEnabled: boolean; 160 | instanceID: number; 161 | } 162 | 163 | export interface MCPGameObjectDetail { 164 | name: string; 165 | instanceID: number; 166 | path: string; 167 | active: boolean; 168 | activeInHierarchy: boolean; 169 | tag: string; 170 | layer: number; 171 | layerName: string; 172 | isStatic: boolean; 173 | transform: MCPTransformInfo; 174 | components: MCPComponentInfo[]; 175 | } 176 | 177 | export enum SceneInfoDetail { 178 | RootObjectsOnly = 'RootObjectsOnly', 179 | FullHierarchy = 'FullHierarchy' 180 | } 181 | 182 | export enum GameObjectInfoDetail { 183 | BasicInfo = 'BasicInfo', 184 | IncludeComponents = 'IncludeComponents', 185 | IncludeChildren = 'IncludeChildren', 186 | IncludeComponentsAndChildren = 'IncludeComponentsAndChildren' 187 | } -------------------------------------------------------------------------------- /mcpServer/src/types.ts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ed25b5070b5a60d40bc257f7ab7b4d23 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/src/websocketHandler.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketServer, WebSocket } from 'ws'; 2 | import { 3 | UnityMessage, 4 | UnityEditorState, 5 | LogEntry, 6 | CommandPromise 7 | } from './types.js'; 8 | 9 | export class WebSocketHandler { 10 | private wsServer!: WebSocketServer; // Add definite assignment assertion 11 | private _port: number; // Make this a private field, not readonly 12 | private unityConnection: WebSocket | null = null; 13 | private editorState: UnityEditorState = { 14 | activeGameObjects: [], 15 | selectedObjects: [], 16 | playModeState: 'Stopped', 17 | sceneHierarchy: {} 18 | }; 19 | 20 | private logBuffer: LogEntry[] = []; 21 | private readonly maxLogBufferSize = 1000; 22 | private commandResultPromise: CommandPromise | null = null; 23 | private commandStartTime: number | null = null; 24 | private lastHeartbeat: number = 0; 25 | private connectionEstablished: boolean = false; 26 | private pendingRequests: Record void; 28 | reject: (reason?: any) => void; 29 | type: string; 30 | }> = {}; 31 | 32 | constructor(port: number = 5010) { 33 | this._port = port; // Store in private field 34 | this.initializeWebSocketServer(port); 35 | } 36 | 37 | // Add a getter to expose port as readonly 38 | public get port(): number { 39 | return this._port; 40 | } 41 | 42 | private initializeWebSocketServer(port: number): void { 43 | try { 44 | this.wsServer = new WebSocketServer({ port }); 45 | this.setupWebSocketServer(); 46 | console.error(`[Unity MCP] WebSocket server started on port ${this._port}`); 47 | } catch (error) { 48 | console.error(`[Unity MCP] ERROR starting WebSocket server on port ${port}:`, error); 49 | this.tryAlternativePort(port); 50 | } 51 | } 52 | 53 | private tryAlternativePort(originalPort: number): void { 54 | try { 55 | const alternativePort = originalPort + 1; 56 | console.error(`[Unity MCP] Trying alternative port ${alternativePort}...`); 57 | this._port = alternativePort; // Update the private field instead of readonly property 58 | this.wsServer = new WebSocketServer({ port: alternativePort }); 59 | this.setupWebSocketServer(); 60 | console.error(`[Unity MCP] WebSocket server started on alternative port ${this._port}`); 61 | } catch (secondError) { 62 | console.error(`[Unity MCP] FATAL: Could not start WebSocket server:`, secondError); 63 | throw new Error(`Failed to start WebSocket server: ${secondError}`); 64 | } 65 | } 66 | 67 | private setupWebSocketServer() { 68 | console.error(`[Unity MCP] WebSocket server starting on port ${this._port}`); 69 | 70 | this.wsServer.on('listening', () => { 71 | console.error('[Unity MCP] WebSocket server is listening for connections'); 72 | }); 73 | 74 | this.wsServer.on('error', (error) => { 75 | console.error('[Unity MCP] WebSocket server error:', error); 76 | }); 77 | 78 | this.wsServer.on('connection', this.handleNewConnection.bind(this)); 79 | } 80 | 81 | private handleNewConnection(ws: WebSocket): void { 82 | console.error('[Unity MCP] Unity Editor connected'); 83 | this.unityConnection = ws; 84 | this.connectionEstablished = true; 85 | this.lastHeartbeat = Date.now(); 86 | 87 | // Send a simple handshake message to verify connection 88 | this.sendHandshake(); 89 | 90 | ws.on('message', (data) => this.handleIncomingMessage(data)); 91 | 92 | ws.on('error', (error) => { 93 | console.error('[Unity MCP] WebSocket error:', error); 94 | this.connectionEstablished = false; 95 | }); 96 | 97 | ws.on('close', () => { 98 | console.error('[Unity MCP] Unity Editor disconnected'); 99 | this.unityConnection = null; 100 | this.connectionEstablished = false; 101 | }); 102 | 103 | // Keep the automatic heartbeat for internal connection validation 104 | const pingInterval = setInterval(() => { 105 | if (ws.readyState === WebSocket.OPEN) { 106 | this.sendPing(); 107 | } else { 108 | clearInterval(pingInterval); 109 | } 110 | }, 30000); // Send heartbeat every 30 seconds 111 | } 112 | 113 | private handleIncomingMessage(data: any): void { 114 | try { 115 | // Update heartbeat on any message 116 | this.lastHeartbeat = Date.now(); 117 | 118 | const message = JSON.parse(data.toString()); 119 | console.error('[Unity MCP] Received message type:', message.type); 120 | 121 | this.handleUnityMessage(message); 122 | } catch (error) { 123 | console.error('[Unity MCP] Error handling message:', error); 124 | } 125 | } 126 | 127 | private sendHandshake() { 128 | this.sendToUnity({ 129 | type: 'handshake', 130 | data: { message: 'MCP Server Connected' } 131 | }); 132 | } 133 | 134 | // Renamed from sendHeartbeat to sendPing for consistency with protocol 135 | private sendPing() { 136 | this.sendToUnity({ 137 | type: "ping", 138 | data: { timestamp: Date.now() } 139 | }); 140 | } 141 | 142 | // Helper method to safely send messages to Unity 143 | private sendToUnity(message: any): void { 144 | try { 145 | if (this.unityConnection && this.unityConnection.readyState === WebSocket.OPEN) { 146 | this.unityConnection.send(JSON.stringify(message)); 147 | } 148 | } catch (error) { 149 | console.error(`[Unity MCP] Error sending message: ${error}`); 150 | this.connectionEstablished = false; 151 | } 152 | } 153 | 154 | private handleUnityMessage(message: UnityMessage) { 155 | switch (message.type) { 156 | case 'editorState': 157 | this.editorState = message.data; 158 | break; 159 | 160 | case 'commandResult': 161 | // Resolve the pending command result promise 162 | if (this.commandResultPromise) { 163 | this.commandResultPromise.resolve(message.data); 164 | this.commandResultPromise = null; 165 | this.commandStartTime = null; 166 | } 167 | break; 168 | 169 | case 'log': 170 | this.addLogEntry(message.data); 171 | break; 172 | 173 | case 'pong': 174 | // Update heartbeat reception timestamp when receiving pong 175 | this.lastHeartbeat = Date.now(); 176 | this.connectionEstablished = true; 177 | break; 178 | 179 | case 'sceneInfo': 180 | case 'gameObjectsDetails': 181 | this.handleRequestResponse(message); 182 | break; 183 | 184 | default: 185 | console.error('[Unity MCP] Unknown message type:', message); 186 | break; 187 | } 188 | } 189 | 190 | private handleRequestResponse(message: UnityMessage): void { 191 | const requestId = message.data?.requestId; 192 | if (requestId && this.pendingRequests[requestId]) { 193 | // Fix the type issue by checking the property exists first 194 | if (this.pendingRequests[requestId]) { 195 | this.pendingRequests[requestId].resolve(message.data); 196 | delete this.pendingRequests[requestId]; 197 | } 198 | } 199 | } 200 | 201 | private addLogEntry(logEntry: LogEntry) { 202 | // Add to buffer, removing oldest if at capacity 203 | this.logBuffer.push(logEntry); 204 | if (this.logBuffer.length > this.maxLogBufferSize) { 205 | this.logBuffer.shift(); 206 | } 207 | } 208 | 209 | public async executeEditorCommand(code: string, timeoutMs: number = 5000): Promise { 210 | if (!this.isConnected()) { 211 | throw new Error('Unity Editor is not connected'); 212 | } 213 | 214 | try { 215 | // Start timing the command execution 216 | this.commandStartTime = Date.now(); 217 | 218 | // Send the command to Unity 219 | this.sendToUnity({ 220 | type: 'executeEditorCommand', 221 | data: { code } 222 | }); 223 | 224 | // Wait for result with timeout 225 | return await Promise.race([ 226 | new Promise((resolve, reject) => { 227 | this.commandResultPromise = { resolve, reject }; 228 | }), 229 | new Promise((_, reject) => 230 | setTimeout(() => reject(new Error( 231 | `Command execution timed out after ${timeoutMs/1000} seconds` 232 | )), timeoutMs) 233 | ) 234 | ]); 235 | } catch (error) { 236 | // Reset command promise state if there's an error 237 | this.commandResultPromise = null; 238 | this.commandStartTime = null; 239 | throw error; 240 | } 241 | } 242 | 243 | // Return the current editor state - only used by tools, doesn't request updates 244 | public getEditorState(): UnityEditorState { 245 | return this.editorState; 246 | } 247 | 248 | public getLogEntries(options: { 249 | types?: string[], 250 | count?: number, 251 | fields?: string[], 252 | messageContains?: string, 253 | stackTraceContains?: string, 254 | timestampAfter?: string, 255 | timestampBefore?: string 256 | } = {}): Partial[] { 257 | const { 258 | types, 259 | count = 100, 260 | fields, 261 | messageContains, 262 | stackTraceContains, 263 | timestampAfter, 264 | timestampBefore 265 | } = options; 266 | 267 | // Apply all filters 268 | let filteredLogs = this.filterLogs(types, messageContains, stackTraceContains, 269 | timestampAfter, timestampBefore); 270 | 271 | // Apply count limit 272 | filteredLogs = filteredLogs.slice(-count); 273 | 274 | // Apply field selection if specified 275 | if (fields?.length) { 276 | return this.selectFields(filteredLogs, fields); 277 | } 278 | 279 | return filteredLogs; 280 | } 281 | 282 | private filterLogs(types?: string[], messageContains?: string, 283 | stackTraceContains?: string, timestampAfter?: string, 284 | timestampBefore?: string): LogEntry[] { 285 | return this.logBuffer.filter(log => { 286 | // Type filter 287 | if (types && !types.includes(log.logType)) return false; 288 | 289 | // Message content filter 290 | if (messageContains && !log.message.includes(messageContains)) return false; 291 | 292 | // Stack trace content filter 293 | if (stackTraceContains && !log.stackTrace.includes(stackTraceContains)) return false; 294 | 295 | // Timestamp filters 296 | if (timestampAfter && new Date(log.timestamp) < new Date(timestampAfter)) return false; 297 | if (timestampBefore && new Date(log.timestamp) > new Date(timestampBefore)) return false; 298 | 299 | return true; 300 | }); 301 | } 302 | 303 | private selectFields(logs: LogEntry[], fields: string[]): Partial[] { 304 | return logs.map(log => { 305 | const selectedFields: Partial = {}; 306 | fields.forEach(field => { 307 | if (field in log) { 308 | selectedFields[field as keyof LogEntry] = log[field as keyof LogEntry]; 309 | } 310 | }); 311 | return selectedFields; 312 | }); 313 | } 314 | 315 | public isConnected(): boolean { 316 | // More robust connection check 317 | if (this.unityConnection === null || this.unityConnection.readyState !== WebSocket.OPEN) { 318 | return false; 319 | } 320 | 321 | // Check if we've received messages from Unity recently 322 | if (!this.connectionEstablished) { 323 | return false; 324 | } 325 | 326 | // Check if we've received a heartbeat in the last 60 seconds 327 | const heartbeatTimeout = 60000; // 60 seconds 328 | if (Date.now() - this.lastHeartbeat > heartbeatTimeout) { 329 | console.error('[Unity MCP] Connection may be stale - no recent communication'); 330 | return false; 331 | } 332 | 333 | return true; 334 | } 335 | 336 | public requestEditorState() { 337 | this.sendToUnity({ 338 | type: 'requestEditorState', 339 | data: {} 340 | }); 341 | } 342 | 343 | public async requestSceneInfo(detailLevel: string): Promise { 344 | return this.makeUnityRequest('getSceneInfo', { detailLevel }, 'sceneInfo'); 345 | } 346 | 347 | public async requestGameObjectsInfo(instanceIDs: number[], detailLevel: string): Promise { 348 | return this.makeUnityRequest('getGameObjectsInfo', { instanceIDs, detailLevel }, 'gameObjectDetails'); 349 | } 350 | 351 | private async makeUnityRequest(type: string, data: any, resultField: string): Promise { 352 | if (!this.isConnected()) { 353 | throw new Error('Unity Editor is not connected'); 354 | } 355 | 356 | const requestId = crypto.randomUUID(); 357 | data.requestId = requestId; 358 | 359 | // Create a promise that will be resolved when we get the response 360 | const responsePromise = new Promise((resolve, reject) => { 361 | const timeout = setTimeout(() => { 362 | delete this.pendingRequests[requestId]; 363 | reject(new Error(`Request for ${type} timed out`)); 364 | }, 10000); // 10 second timeout 365 | 366 | this.pendingRequests[requestId] = { 367 | resolve: (data) => { 368 | clearTimeout(timeout); 369 | resolve(data[resultField]); 370 | }, 371 | reject, 372 | type 373 | }; 374 | }); 375 | 376 | // Send the request to Unity 377 | this.sendToUnity({ 378 | type, 379 | data 380 | }); 381 | 382 | return responsePromise; 383 | } 384 | 385 | // Support for file system tools by adding a method to send generic messages 386 | public async sendMessage(message: string | object) { 387 | if (this.unityConnection && this.unityConnection.readyState === WebSocket.OPEN) { 388 | const messageStr = typeof message === 'string' ? message : JSON.stringify(message); 389 | 390 | return new Promise((resolve, reject) => { 391 | this.unityConnection!.send(messageStr, (err) => { 392 | if (err) { 393 | reject(err); 394 | } else { 395 | resolve(); 396 | } 397 | }); 398 | }); 399 | } 400 | return Promise.resolve(); 401 | } 402 | 403 | public async close() { 404 | if (this.unityConnection) { 405 | try { 406 | this.unityConnection.close(); 407 | } catch (error) { 408 | console.error('[Unity MCP] Error closing Unity connection:', error); 409 | } 410 | this.unityConnection = null; 411 | } 412 | 413 | return new Promise((resolve) => { 414 | try { 415 | this.wsServer.close(() => { 416 | console.error('[Unity MCP] WebSocket server closed'); 417 | resolve(); 418 | }); 419 | } catch (error) { 420 | console.error('[Unity MCP] Error closing WebSocket server:', error); 421 | resolve(); // Resolve anyway to allow the process to exit 422 | } 423 | }); 424 | } 425 | } -------------------------------------------------------------------------------- /mcpServer/src/websocketHandler.ts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9f42bf822d23abd40b0f2ce85a9c17a6 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /mcpServer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "useUnknownInCatchVariables": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules"] 17 | } -------------------------------------------------------------------------------- /mcpServer/tsconfig.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 667f95f8773dee74a8f99cda8368f9be 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.quaza.unitymcp", 3 | "version": "0.0.1", 4 | "displayName": "Unity MCP Integration", 5 | "description": "Integration between Model Context Protocol (MCP) and Unity, allowing AI assistants to understand and interact with Unity projects in real-time. Provides access to scene hierarchy, project settings, and code execution in the Unity Editor.", 6 | "unity": "2021.3", 7 | "dependencies": { 8 | 9 | }, 10 | "keywords": [ 11 | "ai", 12 | "Model Context Protocol", 13 | "Unity AI Tool", 14 | "Unity MCP" 15 | ], 16 | "author": { 17 | "name": "Shahzad", 18 | "url": "https://quazaai.com/" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6bbec64100104e242a883a8a8a668669 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------