Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Editor/Tools/AddPackageTool.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
1 change: 0 additions & 1 deletion Editor/Tools/LoadSceneTool.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using UnityEngine;
using UnityEditor;
using Newtonsoft.Json.Linq;
using McpUnity.Unity;
Expand Down
225 changes: 225 additions & 0 deletions Editor/Tools/RecompileScriptsTool.cs
Original file line number Diff line number Diff line change
@@ -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 {
/// <summary>
/// Tool to recompile all scripts in the Unity project
/// </summary>
public class RecompileScriptsTool : McpToolBase
{
private class CompilationRequest
{
public readonly bool ReturnWithLogs;
public readonly int LogsLimit;
public readonly TaskCompletionSource<JObject> CompletionSource;

public CompilationRequest(bool returnWithLogs, int logsLimit, TaskCompletionSource<JObject> completionSource)
{
ReturnWithLogs = returnWithLogs;
LogsLimit = logsLimit;
CompletionSource = completionSource;
}
}

private class CompilationResult
{
public readonly List<CompilerMessage> SortedLogs;
public readonly int WarningsCount;
public readonly int ErrorsCount;

public bool HasErrors => ErrorsCount > 0;

public CompilationResult(List<CompilerMessage> sortedLogs, int warningsCount, int errorsCount)
{
SortedLogs = sortedLogs;
WarningsCount = warningsCount;
ErrorsCount = errorsCount;
}
}

private readonly List<CompilationRequest> _pendingRequests = new List<CompilationRequest>();
private readonly List<CompilerMessage> _compilationLogs = new List<CompilerMessage>();
private int _processedAssemblies = 0;

public RecompileScriptsTool()
{
Name = "recompile_scripts";
Description = "Recompiles all scripts in the Unity project";
IsAsync = true; // Compilation is asynchronous
}

/// <summary>
/// Execute the Recompile tool asynchronously
/// </summary>
/// <param name="parameters">Tool parameters as a JObject</param>
/// <param name="tcs">TaskCompletionSource to set the result or exception</param>
public override void ExecuteAsync(JObject parameters, TaskCompletionSource<JObject> 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();
}
}

/// <summary>
/// Subscribe to compilation events, reset tracked state
/// </summary>
private void StartCompilationTracking()
{
_compilationLogs.Clear();
_processedAssemblies = 0;
CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished;
CompilationPipeline.compilationFinished += OnCompilationFinished;
}

/// <summary>
/// Unsubscribe from compilation events
/// </summary>
private void StopCompilationTracking()
{
CompilationPipeline.assemblyCompilationFinished -= OnAssemblyCompilationFinished;
CompilationPipeline.compilationFinished -= OnCompilationFinished;
}

/// <summary>
/// Record compilation logs for every single assembly
/// </summary>
private void OnAssemblyCompilationFinished(string assemblyPath, CompilerMessage[] messages)
{
_processedAssemblies++;
_compilationLogs.AddRange(messages);
}

/// <summary>
/// Stop tracking and complete all pending requests
/// </summary>
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<CompilerMessage> 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<CompilationRequest> requestsToComplete = new List<CompilationRequest>();

lock (_pendingRequests)
{
requestsToComplete.AddRange(_pendingRequests);
_pendingRequests.Clear();
}

foreach (var request in requestsToComplete)
{
CompleteRequest(request, result);
}
}

/// <summary>
/// Process a completed compilation request
/// </summary>
private static void CompleteRequest(CompilationRequest request, CompilationResult result)
{
JArray logsArray = new JArray();
IEnumerable<CompilerMessage> logsToReturn = request.ReturnWithLogs ? result.SortedLogs.Take(request.LogsLimit) : Enumerable.Empty<CompilerMessage>();

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);
}

/// <summary>
/// Helper method to safely extract integer parameters with default values
/// </summary>
/// <param name="parameters">JObject containing parameters</param>
/// <param name="key">Parameter key to extract</param>
/// <param name="defaultValue">Default value if parameter is missing or invalid</param>
/// <returns>Extracted integer value or default</returns>
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;
}

/// <summary>
/// Helper method to safely extract boolean parameters with default values
/// </summary>
/// <param name="parameters">JObject containing parameters</param>
/// <param name="key">Parameter key to extract</param>
/// <param name="defaultValue">Default value if parameter is missing or invalid</param>
/// <returns>Extracted boolean value or default</returns>
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;
}
}
}
3 changes: 3 additions & 0 deletions Editor/Tools/RecompileScriptsTool.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Editor/UnityBridge/McpUnityEditorWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 11 additions & 9 deletions Editor/UnityBridge/McpUnityServer.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEditor;
using McpUnity.Tools;
using McpUnity.Resources;
using McpUnity.Services;
using McpUnity.Utils;
using WebSocketSharp.Server;
using System.IO;
using System.Diagnostics;
using System.Net.Sockets;
using UnityEditor.Callbacks;

namespace McpUnity.Unity
{
Expand All @@ -30,16 +29,15 @@ public class McpUnityServer : IDisposable
private CancellationTokenSource _cts;
private TestRunnerService _testRunnerService;
private ConsoleLogsService _consoleLogsService;

/// <summary>
/// Static constructor that gets called when Unity loads due to InitializeOnLoad attribute
/// Called after every domain reload
/// </summary>
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;
}

/// <summary>
Expand Down Expand Up @@ -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);
}

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions README-ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -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エディターで利用可能なすべてのメニュー項目のリストを取得
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 工具
Expand Down
2 changes: 2 additions & 0 deletions Server~/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
Loading