Skip to main content
Glama

Unity MCP Integration

MCPMessageHandler.cs14.8 kB
using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using UnityEditor; using UnityEngine; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Linq; namespace Plugins.GamePilot.Editor.MCP { public class MCPMessageHandler { private readonly MCPDataCollector dataCollector; private readonly MCPCodeExecutor codeExecutor; private readonly MCPMessageSender messageSender; public MCPMessageHandler(MCPDataCollector dataCollector, MCPMessageSender messageSender) { this.dataCollector = dataCollector ?? throw new ArgumentNullException(nameof(dataCollector)); this.messageSender = messageSender ?? throw new ArgumentNullException(nameof(messageSender)); this.codeExecutor = new MCPCodeExecutor(); } public async void HandleMessage(string messageJson) { if (string.IsNullOrEmpty(messageJson)) return; try { Debug.Log($"[MCP] Received message: {messageJson}"); var message = JsonConvert.DeserializeObject<MCPMessage>(messageJson); if (message == null) return; switch (message.Type) { case "selectGameObject": await HandleSelectGameObjectAsync(message.Data); break; case "togglePlayMode": await HandleTogglePlayModeAsync(); break; case "executeEditorCommand": await HandleExecuteCommandAsync(message.Data); break; case "requestEditorState": // Consolidated to a single message type await HandleRequestEditorStateAsync(message.Data); break; case "getLogs": await HandleGetLogsAsync(message.Data); break; case "handshake": await HandleHandshakeAsync(message.Data); break; case "getSceneInfo": await HandleGetSceneInfoAsync(message.Data); break; case "getGameObjectsInfo": await HandleGetGameObjectsInfoAsync(message.Data); break; case "ping": // Renamed from 'heartbeat' to 'ping' to match protocol await HandlePingAsync(message.Data); break; default: Debug.LogWarning($"[MCP] Unknown message type: {message.Type}"); break; } } catch (Exception ex) { Debug.LogError($"[MCP] Error handling message: {ex.Message}\nMessage: {messageJson}"); } } private async Task HandleHandshakeAsync(JToken data) { try { string message = data["message"]?.ToString() ?? "Server connected"; Debug.Log($"[MCP] Handshake received: {message}"); // Send a simple acknowledgment, but don't send full editor state until requested await messageSender.SendPongAsync(); } catch (Exception ex) { Debug.LogError($"[MCP] Error handling handshake: {ex.Message}"); } } // Add a ping handler to respond to server heartbeats private async Task HandlePingAsync(JToken data) { try { // Simply respond with a pong message await messageSender.SendPongAsync(); } catch (Exception ex) { Debug.LogError($"[MCP] Error handling ping: {ex.Message}"); } } private async Task HandleSelectGameObjectAsync(JToken data) { try { string objectPath = data["path"]?.ToString(); string requestId = data["requestId"]?.ToString(); if (string.IsNullOrEmpty(objectPath)) return; var obj = GameObject.Find(objectPath); if (obj != null) { Selection.activeGameObject = obj; Debug.Log($"[MCP] Selected GameObject: {objectPath}"); // If requestId was provided, send back object details if (!string.IsNullOrEmpty(requestId)) { // Use the new method to send details for a single GameObject await messageSender.SendGameObjectDetailsAsync(requestId, obj); } } else { Debug.LogWarning($"[MCP] GameObject not found: {objectPath}"); if (!string.IsNullOrEmpty(requestId)) { await messageSender.SendErrorMessageAsync("OBJECT_NOT_FOUND", $"GameObject not found: {objectPath}"); } } } catch (Exception ex) { Debug.LogError($"[MCP] Error selecting GameObject: {ex.Message}"); } } private async Task HandleTogglePlayModeAsync() { try { EditorApplication.isPlaying = !EditorApplication.isPlaying; Debug.Log($"[MCP] Toggled play mode to: {EditorApplication.isPlaying}"); // Send updated editor state after toggling play mode var editorState = dataCollector.GetEditorState(); await messageSender.SendEditorStateAsync(editorState); } catch (Exception ex) { Debug.LogError($"[MCP] Error toggling play mode: {ex.Message}"); } } private async Task HandleExecuteCommandAsync(JToken data) { try { // Support both old and new parameter naming string commandId = data["commandId"]?.ToString() ?? data["id"]?.ToString() ?? Guid.NewGuid().ToString(); string code = data["code"]?.ToString(); if (string.IsNullOrEmpty(code)) { Debug.LogWarning("[MCP] Received empty code to execute"); await messageSender.SendErrorMessageAsync("EMPTY_CODE", "Received empty code to execute"); return; } Debug.Log($"[MCP] Executing command: {commandId}\n{code}"); var result = codeExecutor.ExecuteCode(code); // Send back the results await messageSender.SendCommandResultAsync( commandId, result, codeExecutor.GetLogs(), codeExecutor.GetErrors(), codeExecutor.GetWarnings() ); Debug.Log($"[MCP] Command execution completed"); } catch (Exception ex) { Debug.LogError($"[MCP] Error executing command: {ex.Message}"); } } // Renamed from HandleGetEditorStateAsync to HandleRequestEditorStateAsync for clarity private async Task HandleRequestEditorStateAsync(JToken data) { try { // Get current editor state with enhanced project info var editorState = GetEnhancedEditorState(); // Send it to the server await messageSender.SendEditorStateAsync(editorState); } catch (Exception ex) { Debug.LogError($"[MCP] Error getting editor state: {ex.Message}"); } } // New method to get enhanced editor state with more project information private MCPEditorState GetEnhancedEditorState() { // Get base editor state from data collector var state = dataCollector.GetEditorState(); // Add additional project information EnhanceEditorStateWithProjectInfo(state); return state; } // Add additional project information to the editor state private void EnhanceEditorStateWithProjectInfo(MCPEditorState state) { try { // Add information about current rendering pipeline state.RenderPipeline = GetCurrentRenderPipeline(); // Add current build target platform state.BuildTarget = EditorUserBuildSettings.activeBuildTarget.ToString(); // Add project name state.ProjectName = Application.productName; // Add graphics API info state.GraphicsDeviceType = SystemInfo.graphicsDeviceType.ToString(); // Add Unity version state.UnityVersion = Application.unityVersion; // Add current scene name var currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); state.CurrentSceneName = currentScene.name; state.CurrentScenePath = currentScene.path; } catch (Exception ex) { Debug.LogError($"[MCP] Error enhancing editor state: {ex.Message}"); } } // Helper to determine current render pipeline private string GetCurrentRenderPipeline() { if (UnityEngine.Rendering.GraphicsSettings.defaultRenderPipeline == null) return "Built-in Render Pipeline"; var pipelineType = UnityEngine.Rendering.GraphicsSettings.defaultRenderPipeline.GetType().Name; // Try to make the name more user-friendly if (pipelineType.Contains("Universal")) return "Universal Render Pipeline (URP)"; else if (pipelineType.Contains("HD")) return "High Definition Render Pipeline (HDRP)"; else if (pipelineType.Contains("Lightweight")) return "Lightweight Render Pipeline (LWRP)"; else return pipelineType; } private async Task HandleGetLogsAsync(JToken data) { try { string requestId = data["requestId"]?.ToString() ?? Guid.NewGuid().ToString(); int count = data["count"]?.Value<int>() ?? 50; // Get logs from collector var logs = dataCollector.GetRecentLogs(count); // Send logs back to server await messageSender.SendGetLogsResponseAsync(requestId, logs); } catch (Exception ex) { Debug.LogError($"[MCP] Error getting logs: {ex.Message}"); } } private async Task HandleGetSceneInfoAsync(JToken data) { try { string requestId = data["requestId"]?.ToString() ?? Guid.NewGuid().ToString(); string detailLevelStr = data["detailLevel"]?.ToString() ?? "RootObjectsOnly"; // Parse the detail level SceneInfoDetail detailLevel; if (!Enum.TryParse(detailLevelStr, true, out detailLevel)) { detailLevel = SceneInfoDetail.RootObjectsOnly; } // Get scene info var sceneInfo = dataCollector.GetCurrentSceneInfo(detailLevel); // Send it to the server await messageSender.SendSceneInfoAsync(requestId, sceneInfo); } catch (Exception ex) { Debug.LogError($"[MCP] Error handling getSceneInfo: {ex.Message}"); await messageSender.SendErrorMessageAsync("SCENE_INFO_ERROR", ex.Message); } } private async Task HandleGetGameObjectsInfoAsync(JToken data) { try { string requestId = data["requestId"]?.ToString() ?? Guid.NewGuid().ToString(); string detailLevelStr = data["detailLevel"]?.ToString() ?? "BasicInfo"; // Get the list of instance IDs int[] instanceIDs; if (data["instanceIDs"] != null && data["instanceIDs"].Type == JTokenType.Array) { instanceIDs = data["instanceIDs"].ToObject<int[]>(); } else { await messageSender.SendErrorMessageAsync("INVALID_PARAMS", "instanceIDs array is required"); return; } // Parse the detail level GameObjectInfoDetail detailLevel; if (!Enum.TryParse(detailLevelStr, true, out detailLevel)) { detailLevel = GameObjectInfoDetail.BasicInfo; } // Get game object details var gameObjectDetails = dataCollector.GetGameObjectsInfo(instanceIDs, detailLevel); // Send to server await messageSender.SendGameObjectsDetailsAsync(requestId, gameObjectDetails); } catch (Exception ex) { Debug.LogError($"[MCP] Error handling getGameObjectsInfo: {ex.Message}"); await messageSender.SendErrorMessageAsync("GAME_OBJECT_INFO_ERROR", ex.Message); } } } internal class MCPMessage { [JsonProperty("type")] public string Type { get; set; } [JsonProperty("data")] public JToken Data { get; set; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/quazaai/UnityMCPIntegration'

If you have feedback or need assistance with the MCP directory API, please join our Discord server