Skip to content

Commit 8c92074

Browse files
committed
feat: add recompile scripts tool
1 parent 7a061ae commit 8c92074

File tree

8 files changed

+289
-4
lines changed

8 files changed

+289
-4
lines changed

Editor/Tools/AddPackageTool.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
using System;
2-
using System.Linq;
32
using System.Collections.Generic;
4-
using UnityEngine;
53
using UnityEditor;
64
using UnityEditor.PackageManager;
75
using UnityEditor.PackageManager.Requests;
86
using Newtonsoft.Json.Linq;
97
using System.Threading.Tasks;
10-
using McpUnity.Tools;
118
using McpUnity.Unity;
129
using McpUnity.Utils;
1310

Editor/Tools/LoadSceneTool.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using UnityEngine;
32
using UnityEditor;
43
using Newtonsoft.Json.Linq;
54
using McpUnity.Unity;
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using McpUnity.Unity;
6+
using McpUnity.Utils;
7+
using Newtonsoft.Json.Linq;
8+
using UnityEditor;
9+
using UnityEditor.Compilation;
10+
using UnityEngine;
11+
12+
namespace McpUnity.Tools {
13+
public class RecompileScriptsTool : McpToolBase
14+
{
15+
private readonly List<CompilerMessage> _compilationLogs = new List<CompilerMessage>();
16+
private TaskCompletionSource<JObject> _completionSource;
17+
private int _processedAssemblies = 0;
18+
private bool _returnWithLogs = true;
19+
private int _logsLimit = 100;
20+
21+
public RecompileScriptsTool()
22+
{
23+
Name = "recompile_scripts";
24+
Description = "Recompiles all scripts in the Unity project. Use returnWithLogs parameter to control whether compilation logs are returned. Use logsLimit parameter to limit the number of logs returned when returnWithLogs is true.";
25+
IsAsync = true; // Compilation is asynchronous
26+
}
27+
28+
/// <summary>
29+
/// Execute the Recompile tool asynchronously
30+
/// </summary>
31+
/// <param name="parameters">Tool parameters as a JObject</param>
32+
/// <param name="tcs">TaskCompletionSource to set the result or exception</param>
33+
public override void ExecuteAsync(JObject parameters, TaskCompletionSource<JObject> tcs)
34+
{
35+
_completionSource = tcs;
36+
_compilationLogs.Clear();
37+
_processedAssemblies = 0;
38+
39+
// Extract and store parameters
40+
_returnWithLogs = GetBoolParameter(parameters, "returnWithLogs", true);
41+
_logsLimit = Mathf.Clamp(GetIntParameter(parameters, "logsLimit", 100), 0, 1000);
42+
43+
CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished;
44+
CompilationPipeline.compilationFinished += OnCompilationFinished;
45+
46+
if (EditorApplication.isCompiling == false) {
47+
McpLogger.LogInfo($"Recompiling all scripts in the Unity project (logsLimit: {_logsLimit})");
48+
CompilationPipeline.RequestScriptCompilation();
49+
}
50+
else {
51+
McpLogger.LogInfo("Recompilation already in progress. Waiting for completion...");
52+
}
53+
}
54+
55+
private void OnAssemblyCompilationFinished(string assemblyPath, CompilerMessage[] messages)
56+
{
57+
_processedAssemblies++;
58+
_compilationLogs.AddRange(messages);
59+
}
60+
61+
private void OnCompilationFinished(object _)
62+
{
63+
McpLogger.LogInfo($"Recompilation completed. Processed {_processedAssemblies} assemblies with {_compilationLogs.Count} compiler messages");
64+
65+
CompilationPipeline.compilationFinished -= OnCompilationFinished;
66+
CompilationPipeline.assemblyCompilationFinished -= OnAssemblyCompilationFinished;
67+
68+
try
69+
{
70+
// Format logs as JSON array similar to ConsoleLogsService
71+
JArray logsArray = new JArray();
72+
73+
// Separate errors, warnings, and other messages
74+
List<CompilerMessage> errors = _compilationLogs.Where(m => m.type == CompilerMessageType.Error).ToList();
75+
List<CompilerMessage> warnings = _compilationLogs.Where(m => m.type == CompilerMessageType.Warning).ToList();
76+
List<CompilerMessage> others = _compilationLogs.Where(m => m.type != CompilerMessageType.Error && m.type != CompilerMessageType.Warning).ToList();
77+
78+
int errorCount = errors.Count;
79+
int warningCount = warnings.Count;
80+
81+
// Sort logs and apply logsLimit - prioritize errors if logsLimit is restrictive
82+
IEnumerable<CompilerMessage> sortedLogs;
83+
if (!_returnWithLogs || _logsLimit <= 0)
84+
{
85+
sortedLogs = Enumerable.Empty<CompilerMessage>();
86+
}
87+
else
88+
{
89+
// Always include all errors, then warnings, then other messages up to the logsLimit
90+
var selectedLogs = errors.ToList();
91+
var remainingSlots = _logsLimit - selectedLogs.Count;
92+
93+
if (remainingSlots > 0)
94+
{
95+
selectedLogs.AddRange(warnings.Take(remainingSlots));
96+
remainingSlots = _logsLimit - selectedLogs.Count;
97+
}
98+
99+
if (remainingSlots > 0)
100+
{
101+
selectedLogs.AddRange(others.Take(remainingSlots));
102+
}
103+
104+
sortedLogs = selectedLogs;
105+
}
106+
107+
foreach (var message in sortedLogs)
108+
{
109+
var logObject = new JObject
110+
{
111+
["message"] = message.message,
112+
["type"] = message.type.ToString(),
113+
["timestamp"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")
114+
};
115+
116+
// Add file information if available
117+
if (!string.IsNullOrEmpty(message.file))
118+
{
119+
logObject["file"] = message.file;
120+
logObject["line"] = message.line;
121+
logObject["column"] = message.column;
122+
}
123+
124+
logsArray.Add(logObject);
125+
}
126+
127+
bool hasErrors = errorCount > 0;
128+
string summaryMessage = hasErrors
129+
? $"Recompilation completed with {errorCount} error(s) and {warningCount} warning(s)"
130+
: $"Successfully recompiled all scripts with {warningCount} warning(s)";
131+
132+
summaryMessage += $" (returnWithLogs: {_returnWithLogs}, logsLimit: {_logsLimit})";
133+
134+
var response = new JObject
135+
{
136+
["success"] = true,
137+
["type"] = "text",
138+
["message"] = summaryMessage,
139+
["logs"] = logsArray
140+
};
141+
142+
McpLogger.LogInfo($"Setting recompilation result: success={!hasErrors}, errors={errorCount}, warnings={warningCount}");
143+
_completionSource.SetResult(response);
144+
}
145+
catch (Exception ex)
146+
{
147+
McpLogger.LogError($"Error creating recompilation response: {ex.Message}\n{ex.StackTrace}");
148+
_completionSource.SetResult(McpUnitySocketHandler.CreateErrorResponse(
149+
$"Error creating recompilation response: {ex.Message}",
150+
"response_creation_error"
151+
));
152+
}
153+
}
154+
155+
/// <summary>
156+
/// Helper method to safely extract integer parameters with default values
157+
/// </summary>
158+
/// <param name="parameters">JObject containing parameters</param>
159+
/// <param name="key">Parameter key to extract</param>
160+
/// <param name="defaultValue">Default value if parameter is missing or invalid</param>
161+
/// <returns>Extracted integer value or default</returns>
162+
private static int GetIntParameter(JObject parameters, string key, int defaultValue)
163+
{
164+
if (parameters?[key] != null && int.TryParse(parameters[key].ToString(), out int value))
165+
return value;
166+
return defaultValue;
167+
}
168+
169+
/// <summary>
170+
/// Helper method to safely extract boolean parameters with default values
171+
/// </summary>
172+
/// <param name="parameters">JObject containing parameters</param>
173+
/// <param name="key">Parameter key to extract</param>
174+
/// <param name="defaultValue">Default value if parameter is missing or invalid</param>
175+
/// <returns>Extracted boolean value or default</returns>
176+
private static bool GetBoolParameter(JObject parameters, string key, bool defaultValue)
177+
{
178+
if (parameters?[key] != null && bool.TryParse(parameters[key].ToString(), out bool value))
179+
return value;
180+
return defaultValue;
181+
}
182+
}
183+
}

Editor/Tools/RecompileScriptsTool.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/UnityBridge/McpUnityEditorWindow.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,14 @@ private void DrawHelpTab()
401401
WrappedLabel("Add the Player prefab from my project to the current scene", new GUIStyle(EditorStyles.miniLabel) { fontStyle = FontStyle.Italic });
402402
EditorGUILayout.EndVertical();
403403

404+
// recompile_scripts
405+
WrappedLabel("recompile_scripts", EditorStyles.boldLabel);
406+
WrappedLabel("Recompiles all scripts in the Unity project");
407+
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
408+
EditorGUILayout.LabelField("Example prompt:", EditorStyles.miniLabel);
409+
WrappedLabel("Recompile scripts in my project", new GUIStyle(EditorStyles.miniLabel) { fontStyle = FontStyle.Italic });
410+
EditorGUILayout.EndVertical();
411+
404412
EditorGUILayout.EndVertical();
405413

406414
// Available Resources section

Editor/UnityBridge/McpUnityServer.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ private void RegisterTools()
265265
// Register LoadSceneTool
266266
LoadSceneTool loadSceneTool = new LoadSceneTool();
267267
_tools.Add(loadSceneTool.Name, loadSceneTool);
268+
269+
// Register RecompileScriptsTool
270+
RecompileScriptsTool recompileScriptsTool = new RecompileScriptsTool();
271+
_tools.Add(recompileScriptsTool.Name, recompileScriptsTool);
268272
}
269273

270274
/// <summary>

Server~/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { registerUpdateGameObjectTool } from './tools/updateGameObjectTool.js';
1616
import { registerCreatePrefabTool } from './tools/createPrefabTool.js';
1717
import { registerDeleteSceneTool } from './tools/deleteSceneTool.js';
1818
import { registerLoadSceneTool } from './tools/loadSceneTool.js';
19+
import { registerRecompileScriptsTool } from './tools/recompileScriptsTool.js';
1920
import { registerGetMenuItemsResource } from './resources/getMenuItemResource.js';
2021
import { registerGetConsoleLogsResource } from './resources/getConsoleLogsResource.js';
2122
import { registerGetHierarchyResource } from './resources/getScenesHierarchyResource.js';
@@ -63,6 +64,7 @@ registerCreatePrefabTool(server, mcpUnity, toolLogger);
6364
registerCreateSceneTool(server, mcpUnity, toolLogger);
6465
registerDeleteSceneTool(server, mcpUnity, toolLogger);
6566
registerLoadSceneTool(server, mcpUnity, toolLogger);
67+
registerRecompileScriptsTool(server, mcpUnity, toolLogger);
6668

6769
// Register all resources into the MCP server
6870
registerGetTestsResource(server, mcpUnity, resourceLogger);
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import * as z from 'zod';
2+
import { Logger } from '../utils/logger.js';
3+
import { McpUnity } from '../unity/mcpUnity.js';
4+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5+
import { McpUnityError, ErrorType } from '../utils/errors.js';
6+
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
7+
8+
// Constants for the tool
9+
const toolName = 'recompile_scripts';
10+
const toolDescription = 'Recompiles all scripts in the Unity project.';
11+
const paramsSchema = z.object({
12+
returnWithLogs: z.boolean().optional().default(true).describe('Whether to return compilation logs'),
13+
logsLimit: z.number().int().min(0).max(1000).optional().default(100).describe('Maximum number of compilation logs to return')
14+
});
15+
16+
/**
17+
* Creates and registers the Recompile Scripts tool with the MCP server
18+
* This tool allows recompiling all scripts in the Unity project
19+
*
20+
* @param server The MCP server instance to register with
21+
* @param mcpUnity The McpUnity instance to communicate with Unity
22+
* @param logger The logger instance for diagnostic information
23+
*/
24+
export function registerRecompileScriptsTool(server: McpServer, mcpUnity: McpUnity, logger: Logger) {
25+
logger.info(`Registering tool: ${toolName}`);
26+
27+
// Register this tool with the MCP server
28+
server.tool(
29+
toolName,
30+
toolDescription,
31+
paramsSchema.shape,
32+
async (params: any) => {
33+
try {
34+
logger.info(`Executing tool: ${toolName}`, params);
35+
const result = await toolHandler(mcpUnity, params);
36+
logger.info(`Tool execution successful: ${toolName}`);
37+
return result;
38+
} catch (error) {
39+
logger.error(`Tool execution failed: ${toolName}`, error);
40+
throw error;
41+
}
42+
}
43+
);
44+
}
45+
46+
/**
47+
* Handles recompile scripts tool requests
48+
*
49+
* @param mcpUnity The McpUnity instance to communicate with Unity
50+
* @param params The parameters for the tool
51+
* @returns A promise that resolves to the tool execution result
52+
* @throws McpUnityError if the request to Unity fails
53+
*/
54+
async function toolHandler(mcpUnity: McpUnity, params: z.infer<typeof paramsSchema>): Promise<CallToolResult> {
55+
// Validate and prepare parameters
56+
const returnWithLogs = params.returnWithLogs ?? true;
57+
const logsLimit = Math.max(0, Math.min(1000, params.logsLimit || 100));
58+
59+
// Send to Unity with validated parameters
60+
const response = await mcpUnity.sendRequest({
61+
method: toolName,
62+
params: {
63+
returnWithLogs,
64+
logsLimit
65+
}
66+
});
67+
68+
if (!response.success) {
69+
throw new McpUnityError(
70+
ErrorType.TOOL_EXECUTION,
71+
response.message || `Failed to recompile scripts`
72+
);
73+
}
74+
75+
return {
76+
content: [
77+
{
78+
type: 'text',
79+
text: response.message
80+
},
81+
{
82+
type: 'text',
83+
text: JSON.stringify({
84+
logs: response.logs
85+
}, null, 2)
86+
}
87+
]
88+
};
89+
}

0 commit comments

Comments
 (0)