diff --git a/Editor/Tools/AddPackageTool.cs b/Editor/Tools/AddPackageTool.cs index e48d576..90d92c0 100644 --- a/Editor/Tools/AddPackageTool.cs +++ b/Editor/Tools/AddPackageTool.cs @@ -1,13 +1,10 @@ using System; -using System.Linq; using System.Collections.Generic; -using UnityEngine; using UnityEditor; using UnityEditor.PackageManager; using UnityEditor.PackageManager.Requests; using Newtonsoft.Json.Linq; using System.Threading.Tasks; -using McpUnity.Tools; using McpUnity.Unity; using McpUnity.Utils; diff --git a/Editor/Tools/LoadSceneTool.cs b/Editor/Tools/LoadSceneTool.cs index 500be43..df9676d 100644 --- a/Editor/Tools/LoadSceneTool.cs +++ b/Editor/Tools/LoadSceneTool.cs @@ -1,5 +1,4 @@ using System; -using UnityEngine; using UnityEditor; using Newtonsoft.Json.Linq; using McpUnity.Unity; diff --git a/Editor/Tools/RecompileScriptsTool.cs b/Editor/Tools/RecompileScriptsTool.cs new file mode 100644 index 0000000..5ea87ab --- /dev/null +++ b/Editor/Tools/RecompileScriptsTool.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using McpUnity.Utils; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditor.Compilation; +using UnityEngine; + +namespace McpUnity.Tools { + /// + /// Tool to recompile all scripts in the Unity project + /// + public class RecompileScriptsTool : McpToolBase + { + private class CompilationRequest + { + public readonly bool ReturnWithLogs; + public readonly int LogsLimit; + public readonly TaskCompletionSource CompletionSource; + + public CompilationRequest(bool returnWithLogs, int logsLimit, TaskCompletionSource completionSource) + { + ReturnWithLogs = returnWithLogs; + LogsLimit = logsLimit; + CompletionSource = completionSource; + } + } + + private class CompilationResult + { + public readonly List SortedLogs; + public readonly int WarningsCount; + public readonly int ErrorsCount; + + public bool HasErrors => ErrorsCount > 0; + + public CompilationResult(List sortedLogs, int warningsCount, int errorsCount) + { + SortedLogs = sortedLogs; + WarningsCount = warningsCount; + ErrorsCount = errorsCount; + } + } + + private readonly List _pendingRequests = new List(); + private readonly List _compilationLogs = new List(); + private int _processedAssemblies = 0; + + public RecompileScriptsTool() + { + Name = "recompile_scripts"; + Description = "Recompiles all scripts in the Unity project"; + IsAsync = true; // Compilation is asynchronous + } + + /// + /// Execute the Recompile tool asynchronously + /// + /// Tool parameters as a JObject + /// TaskCompletionSource to set the result or exception + public override void ExecuteAsync(JObject parameters, TaskCompletionSource tcs) + { + // Extract and store parameters + var returnWithLogs = GetBoolParameter(parameters, "returnWithLogs", true); + var logsLimit = Mathf.Clamp(GetIntParameter(parameters, "logsLimit", 100), 0, 1000); + var request = new CompilationRequest(returnWithLogs, logsLimit, tcs); + + bool hasActiveRequest = false; + lock (_pendingRequests) + { + hasActiveRequest = _pendingRequests.Count > 0; + _pendingRequests.Add(request); + } + + if (hasActiveRequest) + { + McpLogger.LogInfo("Recompilation already in progress. Waiting for completion..."); + return; + } + + // On first request, initialize compilation listeners and start compilation + StartCompilationTracking(); + + if (EditorApplication.isCompiling == false) + { + McpLogger.LogInfo("Recompiling all scripts in the Unity project"); + CompilationPipeline.RequestScriptCompilation(); + } + } + + /// + /// Subscribe to compilation events, reset tracked state + /// + private void StartCompilationTracking() + { + _compilationLogs.Clear(); + _processedAssemblies = 0; + CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished; + CompilationPipeline.compilationFinished += OnCompilationFinished; + } + + /// + /// Unsubscribe from compilation events + /// + private void StopCompilationTracking() + { + CompilationPipeline.assemblyCompilationFinished -= OnAssemblyCompilationFinished; + CompilationPipeline.compilationFinished -= OnCompilationFinished; + } + + /// + /// Record compilation logs for every single assembly + /// + private void OnAssemblyCompilationFinished(string assemblyPath, CompilerMessage[] messages) + { + _processedAssemblies++; + _compilationLogs.AddRange(messages); + } + + /// + /// Stop tracking and complete all pending requests + /// + private void OnCompilationFinished(object _) + { + McpLogger.LogInfo($"Recompilation completed. Processed {_processedAssemblies} assemblies with {_compilationLogs.Count} compiler messages"); + + // Sort logs by type: first errors, then warnings and info + List sortedLogs = _compilationLogs.OrderBy(x => x.type).ToList(); + int errorsCount = _compilationLogs.Count(l => l.type == CompilerMessageType.Error); + int warningsCount = _compilationLogs.Count(l => l.type == CompilerMessageType.Warning); + CompilationResult result = new CompilationResult(sortedLogs, warningsCount, errorsCount); + + // Stop tracking before completing requests + StopCompilationTracking(); + + // Complete all requests received before compilation end, the next received request will start a new compilation + List requestsToComplete = new List(); + + lock (_pendingRequests) + { + requestsToComplete.AddRange(_pendingRequests); + _pendingRequests.Clear(); + } + + foreach (var request in requestsToComplete) + { + CompleteRequest(request, result); + } + } + + /// + /// Process a completed compilation request + /// + private static void CompleteRequest(CompilationRequest request, CompilationResult result) + { + JArray logsArray = new JArray(); + IEnumerable logsToReturn = request.ReturnWithLogs ? result.SortedLogs.Take(request.LogsLimit) : Enumerable.Empty(); + + foreach (var message in logsToReturn) + { + var logObject = new JObject + { + ["message"] = message.message, + ["type"] = message.type.ToString() + }; + + // Add file information if available + if (!string.IsNullOrEmpty(message.file)) + { + logObject["file"] = message.file; + logObject["line"] = message.line; + logObject["column"] = message.column; + } + + logsArray.Add(logObject); + } + + string summaryMessage = result.HasErrors + ? $"Recompilation completed with {result.ErrorsCount} error(s) and {result.WarningsCount} warning(s)" + : $"Successfully recompiled all scripts with {result.WarningsCount} warning(s)"; + + summaryMessage += $" (returnWithLogs: {request.ReturnWithLogs}, logsLimit: {request.LogsLimit})"; + + var response = new JObject + { + ["success"] = true, + ["type"] = "text", + ["message"] = summaryMessage, + ["logs"] = logsArray + }; + + request.CompletionSource.SetResult(response); + } + + /// + /// Helper method to safely extract integer parameters with default values + /// + /// JObject containing parameters + /// Parameter key to extract + /// Default value if parameter is missing or invalid + /// Extracted integer value or default + private static int GetIntParameter(JObject parameters, string key, int defaultValue) + { + if (parameters?[key] != null && int.TryParse(parameters[key].ToString(), out int value)) + return value; + return defaultValue; + } + + /// + /// Helper method to safely extract boolean parameters with default values + /// + /// JObject containing parameters + /// Parameter key to extract + /// Default value if parameter is missing or invalid + /// Extracted boolean value or default + private static bool GetBoolParameter(JObject parameters, string key, bool defaultValue) + { + if (parameters?[key] != null && bool.TryParse(parameters[key].ToString(), out bool value)) + return value; + return defaultValue; + } + } +} \ No newline at end of file diff --git a/Editor/Tools/RecompileScriptsTool.cs.meta b/Editor/Tools/RecompileScriptsTool.cs.meta new file mode 100644 index 0000000..98c53a5 --- /dev/null +++ b/Editor/Tools/RecompileScriptsTool.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9db4c0c982944b9da2e14745cf799f99 +timeCreated: 1758591273 \ No newline at end of file diff --git a/Editor/UnityBridge/McpUnityEditorWindow.cs b/Editor/UnityBridge/McpUnityEditorWindow.cs index b53a222..d955d5a 100644 --- a/Editor/UnityBridge/McpUnityEditorWindow.cs +++ b/Editor/UnityBridge/McpUnityEditorWindow.cs @@ -401,6 +401,14 @@ private void DrawHelpTab() WrappedLabel("Add the Player prefab from my project to the current scene", new GUIStyle(EditorStyles.miniLabel) { fontStyle = FontStyle.Italic }); EditorGUILayout.EndVertical(); + // recompile_scripts + WrappedLabel("recompile_scripts", EditorStyles.boldLabel); + WrappedLabel("Recompiles all scripts in the Unity project"); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField("Example prompt:", EditorStyles.miniLabel); + WrappedLabel("Recompile scripts in my project", new GUIStyle(EditorStyles.miniLabel) { fontStyle = FontStyle.Italic }); + EditorGUILayout.EndVertical(); + EditorGUILayout.EndVertical(); // Available Resources section diff --git a/Editor/UnityBridge/McpUnityServer.cs b/Editor/UnityBridge/McpUnityServer.cs index d623c2d..f7fd0d9 100644 --- a/Editor/UnityBridge/McpUnityServer.cs +++ b/Editor/UnityBridge/McpUnityServer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Threading; -using UnityEngine; using UnityEditor; using McpUnity.Tools; using McpUnity.Resources; @@ -9,8 +8,8 @@ using McpUnity.Utils; using WebSocketSharp.Server; using System.IO; -using System.Diagnostics; using System.Net.Sockets; +using UnityEditor.Callbacks; namespace McpUnity.Unity { @@ -30,16 +29,15 @@ public class McpUnityServer : IDisposable private CancellationTokenSource _cts; private TestRunnerService _testRunnerService; private ConsoleLogsService _consoleLogsService; - + /// - /// Static constructor that gets called when Unity loads due to InitializeOnLoad attribute + /// Called after every domain reload /// - static McpUnityServer() + [DidReloadScripts] + private static void AfterReload() { - EditorApplication.delayCall += () => { - // Ensure Instance is created and hooks are set up after initial domain load - var currentInstance = Instance; - }; + // Ensure Instance is created and hooks are set up after initial domain load + var currentInstance = Instance; } /// @@ -265,6 +263,10 @@ private void RegisterTools() // Register LoadSceneTool LoadSceneTool loadSceneTool = new LoadSceneTool(); _tools.Add(loadSceneTool.Name, loadSceneTool); + + // Register RecompileScriptsTool + RecompileScriptsTool recompileScriptsTool = new RecompileScriptsTool(); + _tools.Add(recompileScriptsTool.Name, recompileScriptsTool); } /// diff --git a/README-ja.md b/README-ja.md index f64a866..d99c890 100644 --- a/README-ja.md +++ b/README-ja.md @@ -86,6 +86,9 @@ MCP Unityは、Unityの`Library/PackedCache`フォルダーをワークスペー - `create_prefab`: プレハブを作成し、オプションでMonoBehaviourスクリプトとシリアライズされたフィールド値を設定 > **例:** "'PlayerController'スクリプトから'Player'という名前のプレハブを作成" +- `recompile_scripts`: Unityプロジェクト内のすべてのスクリプトを再コンパイル + > **例:** "Unityプロジェクト内のすべてのスクリプトを再コンパイル" + ### MCPサーバーリソース - `unity://menu-items`: `execute_menu_item`ツールを容易にするために、Unityエディターで利用可能なすべてのメニュー項目のリストを取得 diff --git a/README.md b/README.md index db28e90..30547bd 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,9 @@ The following tools are available for manipulating and querying Unity scenes and - `create_prefab`: Creates a prefab with optional MonoBehaviour script and serialized field values > **Example prompt:** "Create a prefab named 'Player' from the 'PlayerController' script" +- `recompile_scripts`: Recompiles all scripts in the Unity project + > **Example prompt:** "Recompile scripts in my Unity project" + ### MCP Server Resources - `unity://menu-items`: Retrieves a list of all available menu items in the Unity Editor to facilitate `execute_menu_item` tool diff --git a/README_zh-CN.md b/README_zh-CN.md index c5a70fb..9b6f5a1 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -87,6 +87,9 @@ MCP Unity 通过将 Unity `Library/PackedCache` 文件夹添加到您的工作 - `create_prefab`: 创建预制体,并可选择添加 MonoBehaviour 脚本和设置序列化字段值 > **示例提示:** "从 'PlayerController' 脚本创建一个名为 'Player' 的预制体" +- `recompile_scripts`: 重新编译 Unity 项目中的所有脚本 + > **示例提示:** "重新编译我 Unity 项目中的所有脚本" + ### MCP 服务器资源 - `unity://menu-items`: 获取 Unity 编辑器中所有可用的菜单项列表,以方便 `execute_menu_item` 工具 diff --git a/Server~/src/index.ts b/Server~/src/index.ts index f5c12c0..3ff840c 100644 --- a/Server~/src/index.ts +++ b/Server~/src/index.ts @@ -16,6 +16,7 @@ import { registerUpdateGameObjectTool } from './tools/updateGameObjectTool.js'; import { registerCreatePrefabTool } from './tools/createPrefabTool.js'; import { registerDeleteSceneTool } from './tools/deleteSceneTool.js'; import { registerLoadSceneTool } from './tools/loadSceneTool.js'; +import { registerRecompileScriptsTool } from './tools/recompileScriptsTool.js'; import { registerGetMenuItemsResource } from './resources/getMenuItemResource.js'; import { registerGetConsoleLogsResource } from './resources/getConsoleLogsResource.js'; import { registerGetHierarchyResource } from './resources/getScenesHierarchyResource.js'; @@ -63,6 +64,7 @@ registerCreatePrefabTool(server, mcpUnity, toolLogger); registerCreateSceneTool(server, mcpUnity, toolLogger); registerDeleteSceneTool(server, mcpUnity, toolLogger); registerLoadSceneTool(server, mcpUnity, toolLogger); +registerRecompileScriptsTool(server, mcpUnity, toolLogger); // Register all resources into the MCP server registerGetTestsResource(server, mcpUnity, resourceLogger); diff --git a/Server~/src/tools/recompileScriptsTool.ts b/Server~/src/tools/recompileScriptsTool.ts new file mode 100644 index 0000000..4b12190 --- /dev/null +++ b/Server~/src/tools/recompileScriptsTool.ts @@ -0,0 +1,89 @@ +import * as z from 'zod'; +import { Logger } from '../utils/logger.js'; +import { McpUnity } from '../unity/mcpUnity.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpUnityError, ErrorType } from '../utils/errors.js'; +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; + +// Constants for the tool +const toolName = 'recompile_scripts'; +const toolDescription = 'Recompiles all scripts in the Unity project.'; +const paramsSchema = z.object({ + returnWithLogs: z.boolean().optional().default(true).describe('Whether to return compilation logs'), + logsLimit: z.number().int().min(0).max(1000).optional().default(100).describe('Maximum number of compilation logs to return') +}); + +/** + * Creates and registers the Recompile Scripts tool with the MCP server + * This tool allows recompiling all scripts in the Unity project + * + * @param server The MCP server instance to register with + * @param mcpUnity The McpUnity instance to communicate with Unity + * @param logger The logger instance for diagnostic information + */ +export function registerRecompileScriptsTool(server: McpServer, mcpUnity: McpUnity, logger: Logger) { + logger.info(`Registering tool: ${toolName}`); + + // Register this tool with the MCP server + server.tool( + toolName, + toolDescription, + paramsSchema.shape, + async (params: any) => { + try { + logger.info(`Executing tool: ${toolName}`, params); + const result = await toolHandler(mcpUnity, params); + logger.info(`Tool execution successful: ${toolName}`); + return result; + } catch (error) { + logger.error(`Tool execution failed: ${toolName}`, error); + throw error; + } + } + ); +} + +/** + * Handles recompile scripts tool requests + * + * @param mcpUnity The McpUnity instance to communicate with Unity + * @param params The parameters for the tool + * @returns A promise that resolves to the tool execution result + * @throws McpUnityError if the request to Unity fails + */ +async function toolHandler(mcpUnity: McpUnity, params: z.infer): Promise { + // Validate and prepare parameters + const returnWithLogs = params.returnWithLogs ?? true; + const logsLimit = Math.max(0, Math.min(1000, params.logsLimit || 100)); + + // Send to Unity with validated parameters + const response = await mcpUnity.sendRequest({ + method: toolName, + params: { + returnWithLogs, + logsLimit + } + }); + + if (!response.success) { + throw new McpUnityError( + ErrorType.TOOL_EXECUTION, + response.message || `Failed to recompile scripts` + ); + } + + return { + content: [ + { + type: 'text', + text: response.message + }, + { + type: 'text', + text: JSON.stringify({ + logs: response.logs + }, null, 2) + } + ] + }; +}