Merge remote-tracking branch 'upstream/master'
commit
f334b0a6db
|
|
@ -2,10 +2,9 @@ using UnityEngine;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using System.Linq;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace MCPServer.Editor.Commands
|
namespace UnityMCP.Editor.Commands
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles asset-related commands for the MCP Server
|
/// Handles asset-related commands for the MCP Server
|
||||||
|
|
@ -51,7 +50,8 @@ namespace MCPServer.Editor.Commands
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (System.Exception e)
|
||||||
{
|
{
|
||||||
return new {
|
return new
|
||||||
|
{
|
||||||
success = false,
|
success = false,
|
||||||
error = $"Failed to import asset: {e.Message}",
|
error = $"Failed to import asset: {e.Message}",
|
||||||
stackTrace = e.StackTrace
|
stackTrace = e.StackTrace
|
||||||
|
|
@ -71,12 +71,12 @@ namespace MCPServer.Editor.Commands
|
||||||
if (string.IsNullOrEmpty(prefabPath))
|
if (string.IsNullOrEmpty(prefabPath))
|
||||||
return new { success = false, error = "Prefab path cannot be empty" };
|
return new { success = false, error = "Prefab path cannot be empty" };
|
||||||
|
|
||||||
Vector3 position = new Vector3(
|
Vector3 position = new(
|
||||||
(float)@params["position_x"],
|
(float)@params["position_x"],
|
||||||
(float)@params["position_y"],
|
(float)@params["position_y"],
|
||||||
(float)@params["position_z"]
|
(float)@params["position_z"]
|
||||||
);
|
);
|
||||||
Vector3 rotation = new Vector3(
|
Vector3 rotation = new(
|
||||||
(float)@params["rotation_x"],
|
(float)@params["rotation_x"],
|
||||||
(float)@params["rotation_y"],
|
(float)@params["rotation_y"],
|
||||||
(float)@params["rotation_z"]
|
(float)@params["rotation_z"]
|
||||||
|
|
@ -106,7 +106,8 @@ namespace MCPServer.Editor.Commands
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (System.Exception e)
|
||||||
{
|
{
|
||||||
return new {
|
return new
|
||||||
|
{
|
||||||
success = false,
|
success = false,
|
||||||
error = $"Failed to instantiate prefab: {e.Message}",
|
error = $"Failed to instantiate prefab: {e.Message}",
|
||||||
stackTrace = e.StackTrace
|
stackTrace = e.StackTrace
|
||||||
|
|
@ -162,7 +163,8 @@ namespace MCPServer.Editor.Commands
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (System.Exception e)
|
||||||
{
|
{
|
||||||
return new {
|
return new
|
||||||
|
{
|
||||||
success = false,
|
success = false,
|
||||||
error = $"Failed to create prefab: {e.Message}",
|
error = $"Failed to create prefab: {e.Message}",
|
||||||
stackTrace = e.StackTrace,
|
stackTrace = e.StackTrace,
|
||||||
|
|
@ -218,9 +220,9 @@ namespace MCPServer.Editor.Commands
|
||||||
assets.Add(new
|
assets.Add(new
|
||||||
{
|
{
|
||||||
name = Path.GetFileNameWithoutExtension(path),
|
name = Path.GetFileNameWithoutExtension(path),
|
||||||
path = path,
|
path,
|
||||||
type = assetType?.Name ?? "Unknown",
|
type = assetType?.Name ?? "Unknown",
|
||||||
guid = guid
|
guid
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace MCPServer.Editor.Commands
|
namespace UnityMCP.Editor.Commands
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registry for all MCP command handlers
|
/// Registry for all MCP command handlers
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,15 @@ using Newtonsoft.Json.Linq;
|
||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq; // Add LINQ namespace for Select extension method
|
using System.Linq;
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
/// <summary>
|
namespace UnityMCP.Editor.Commands
|
||||||
/// Handles editor control commands like undo, redo, play, pause, stop, and build operations.
|
|
||||||
/// </summary>
|
|
||||||
public static class EditorControlHandler
|
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles editor control commands like undo, redo, play, pause, stop, and build operations.
|
||||||
|
/// </summary>
|
||||||
|
public static class EditorControlHandler
|
||||||
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles editor control commands
|
/// Handles editor control commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -21,29 +22,19 @@ public static class EditorControlHandler
|
||||||
string command = (string)@params["command"];
|
string command = (string)@params["command"];
|
||||||
JObject commandParams = (JObject)@params["params"];
|
JObject commandParams = (JObject)@params["params"];
|
||||||
|
|
||||||
switch (command.ToUpper())
|
return command.ToUpper() switch
|
||||||
{
|
{
|
||||||
case "UNDO":
|
"UNDO" => HandleUndo(),
|
||||||
return HandleUndo();
|
"REDO" => HandleRedo(),
|
||||||
case "REDO":
|
"PLAY" => HandlePlay(),
|
||||||
return HandleRedo();
|
"PAUSE" => HandlePause(),
|
||||||
case "PLAY":
|
"STOP" => HandleStop(),
|
||||||
return HandlePlay();
|
"BUILD" => HandleBuild(commandParams),
|
||||||
case "PAUSE":
|
"EXECUTE_COMMAND" => HandleExecuteCommand(commandParams),
|
||||||
return HandlePause();
|
"READ_CONSOLE" => ReadConsole(commandParams),
|
||||||
case "STOP":
|
"GET_AVAILABLE_COMMANDS" => GetAvailableCommands(),
|
||||||
return HandleStop();
|
_ => new { error = $"Unknown editor control command: {command}" },
|
||||||
case "BUILD":
|
};
|
||||||
return HandleBuild(commandParams);
|
|
||||||
case "EXECUTE_COMMAND":
|
|
||||||
return HandleExecuteCommand(commandParams);
|
|
||||||
case "READ_CONSOLE":
|
|
||||||
return ReadConsole(commandParams);
|
|
||||||
case "GET_AVAILABLE_COMMANDS":
|
|
||||||
return GetAvailableCommands();
|
|
||||||
default:
|
|
||||||
return new { error = $"Unknown editor control command: {command}" };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object HandleUndo()
|
private static object HandleUndo()
|
||||||
|
|
@ -101,15 +92,21 @@ public static class EditorControlHandler
|
||||||
return new { error = $"Unsupported platform: {platform}" };
|
return new { error = $"Unsupported platform: {platform}" };
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
|
BuildPlayerOptions buildPlayerOptions = new()
|
||||||
buildPlayerOptions.scenes = GetEnabledScenes();
|
{
|
||||||
buildPlayerOptions.target = target;
|
scenes = GetEnabledScenes(),
|
||||||
buildPlayerOptions.locationPathName = buildPath;
|
target = target,
|
||||||
|
locationPathName = buildPath
|
||||||
|
};
|
||||||
|
|
||||||
BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);
|
BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);
|
||||||
return new { message = "Build completed successfully", summary = report.summary };
|
return new
|
||||||
|
{
|
||||||
|
message = "Build completed successfully",
|
||||||
|
report.summary
|
||||||
|
};
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return new { error = $"Build failed: {e.Message}" };
|
return new { error = $"Build failed: {e.Message}" };
|
||||||
}
|
}
|
||||||
|
|
@ -123,7 +120,7 @@ public static class EditorControlHandler
|
||||||
EditorApplication.ExecuteMenuItem(commandName);
|
EditorApplication.ExecuteMenuItem(commandName);
|
||||||
return new { message = $"Executed command: {commandName}" };
|
return new { message = $"Executed command: {commandName}" };
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return new { error = $"Failed to execute command: {e.Message}" };
|
return new { error = $"Failed to execute command: {e.Message}" };
|
||||||
}
|
}
|
||||||
|
|
@ -145,14 +142,10 @@ public static class EditorControlHandler
|
||||||
// Get filter parameters if provided
|
// Get filter parameters if provided
|
||||||
if (@params != null)
|
if (@params != null)
|
||||||
{
|
{
|
||||||
if (@params["show_logs"] != null)
|
if (@params["show_logs"] != null) showLogs = (bool)@params["show_logs"];
|
||||||
showLogs = (bool)@params["show_logs"];
|
if (@params["show_warnings"] != null) showWarnings = (bool)@params["show_warnings"];
|
||||||
if (@params["show_warnings"] != null)
|
if (@params["show_errors"] != null) showErrors = (bool)@params["show_errors"];
|
||||||
showWarnings = (bool)@params["show_warnings"];
|
if (@params["search_term"] != null) searchTerm = (string)@params["search_term"];
|
||||||
if (@params["show_errors"] != null)
|
|
||||||
showErrors = (bool)@params["show_errors"];
|
|
||||||
if (@params["search_term"] != null)
|
|
||||||
searchTerm = (string)@params["search_term"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -162,50 +155,20 @@ public static class EditorControlHandler
|
||||||
Type logEntryType = Type.GetType("UnityEditor.LogEntry,UnityEditor");
|
Type logEntryType = Type.GetType("UnityEditor.LogEntry,UnityEditor");
|
||||||
|
|
||||||
if (logEntriesType == null || logEntryType == null)
|
if (logEntriesType == null || logEntryType == null)
|
||||||
return new
|
return new { error = "Could not find required Unity logging types", entries = new List<object>() };
|
||||||
{
|
|
||||||
error = "Could not find required Unity logging types",
|
|
||||||
entries = new List<object>()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get essential methods
|
// Get essential methods
|
||||||
MethodInfo getCountMethod = logEntriesType.GetMethod(
|
MethodInfo getCountMethod = logEntriesType.GetMethod("GetCount", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
"GetCount",
|
MethodInfo getEntryMethod = logEntriesType.GetMethod("GetEntryAt", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ??
|
||||||
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
|
logEntriesType.GetMethod("GetEntryInternal", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
);
|
|
||||||
MethodInfo getEntryMethod =
|
|
||||||
logEntriesType.GetMethod(
|
|
||||||
"GetEntryAt",
|
|
||||||
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
|
|
||||||
)
|
|
||||||
?? logEntriesType.GetMethod(
|
|
||||||
"GetEntryInternal",
|
|
||||||
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
|
|
||||||
);
|
|
||||||
|
|
||||||
if (getCountMethod == null || getEntryMethod == null)
|
if (getCountMethod == null || getEntryMethod == null)
|
||||||
return new
|
return new { error = "Could not find required Unity logging methods", entries = new List<object>() };
|
||||||
{
|
|
||||||
error = "Could not find required Unity logging methods",
|
|
||||||
entries = new List<object>()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get stack trace method if available
|
// Get stack trace method if available
|
||||||
MethodInfo getStackTraceMethod =
|
MethodInfo getStackTraceMethod = logEntriesType.GetMethod("GetEntryStackTrace", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
|
||||||
logEntriesType.GetMethod(
|
null, new[] { typeof(int) }, null) ?? logEntriesType.GetMethod("GetStackTrace", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
|
||||||
"GetEntryStackTrace",
|
null, new[] { typeof(int) }, null);
|
||||||
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
|
|
||||||
null,
|
|
||||||
new[] { typeof(int) },
|
|
||||||
null
|
|
||||||
)
|
|
||||||
?? logEntriesType.GetMethod(
|
|
||||||
"GetStackTrace",
|
|
||||||
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
|
|
||||||
null,
|
|
||||||
new[] { typeof(int) },
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get entry count and prepare result list
|
// Get entry count and prepare result list
|
||||||
int count = (int)getCountMethod.Invoke(null, null);
|
int count = (int)getCountMethod.Invoke(null, null);
|
||||||
|
|
@ -215,17 +178,12 @@ public static class EditorControlHandler
|
||||||
object logEntryInstance = Activator.CreateInstance(logEntryType);
|
object logEntryInstance = Activator.CreateInstance(logEntryType);
|
||||||
|
|
||||||
// Find properties on LogEntry type
|
// Find properties on LogEntry type
|
||||||
PropertyInfo modeProperty =
|
PropertyInfo modeProperty = logEntryType.GetProperty("mode") ?? logEntryType.GetProperty("Mode");
|
||||||
logEntryType.GetProperty("mode") ?? logEntryType.GetProperty("Mode");
|
PropertyInfo messageProperty = logEntryType.GetProperty("message") ?? logEntryType.GetProperty("Message");
|
||||||
PropertyInfo messageProperty =
|
|
||||||
logEntryType.GetProperty("message") ?? logEntryType.GetProperty("Message");
|
|
||||||
|
|
||||||
// Parse search terms if provided
|
// Parse search terms if provided
|
||||||
string[] searchWords = !string.IsNullOrWhiteSpace(searchTerm)
|
string[] searchWords = !string.IsNullOrWhiteSpace(searchTerm) ?
|
||||||
? searchTerm
|
searchTerm.ToLower().Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) : null;
|
||||||
.ToLower()
|
|
||||||
.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Process each log entry
|
// Process each log entry
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
|
|
@ -238,41 +196,30 @@ public static class EditorControlHandler
|
||||||
{
|
{
|
||||||
getEntryMethod.Invoke(null, new object[] { i, logEntryInstance });
|
getEntryMethod.Invoke(null, new object[] { i, logEntryInstance });
|
||||||
}
|
}
|
||||||
else if (
|
else if (methodParams.Length >= 1 && methodParams[0].ParameterType == typeof(int))
|
||||||
methodParams.Length >= 1 && methodParams[0].ParameterType == typeof(int)
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var parameters = new object[methodParams.Length];
|
var parameters = new object[methodParams.Length];
|
||||||
parameters[0] = i;
|
parameters[0] = i;
|
||||||
for (int p = 1; p < parameters.Length; p++)
|
for (int p = 1; p < parameters.Length; p++)
|
||||||
{
|
{
|
||||||
parameters[p] = methodParams[p].ParameterType.IsValueType
|
parameters[p] = methodParams[p].ParameterType.IsValueType ?
|
||||||
? Activator.CreateInstance(methodParams[p].ParameterType)
|
Activator.CreateInstance(methodParams[p].ParameterType) : null;
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
getEntryMethod.Invoke(null, parameters);
|
getEntryMethod.Invoke(null, parameters);
|
||||||
}
|
}
|
||||||
else
|
else continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
// Extract log data
|
// Extract log data
|
||||||
int logType =
|
int logType = modeProperty != null ?
|
||||||
modeProperty != null
|
Convert.ToInt32(modeProperty.GetValue(logEntryInstance) ?? 0) : 0;
|
||||||
? Convert.ToInt32(modeProperty.GetValue(logEntryInstance) ?? 0)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
string message =
|
string message = messageProperty != null ?
|
||||||
messageProperty != null
|
(messageProperty.GetValue(logEntryInstance)?.ToString() ?? "") : "";
|
||||||
? (messageProperty.GetValue(logEntryInstance)?.ToString() ?? "")
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// If message is empty, try to get it via a field
|
// If message is empty, try to get it via a field
|
||||||
if (string.IsNullOrEmpty(message))
|
if (string.IsNullOrEmpty(message))
|
||||||
{
|
{
|
||||||
var msgField = logEntryType.GetField(
|
var msgField = logEntryType.GetField("message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
"message",
|
|
||||||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance
|
|
||||||
);
|
|
||||||
if (msgField != null)
|
if (msgField != null)
|
||||||
{
|
{
|
||||||
object msgValue = msgField.GetValue(logEntryInstance);
|
object msgValue = msgField.GetValue(logEntryInstance);
|
||||||
|
|
@ -283,93 +230,50 @@ public static class EditorControlHandler
|
||||||
if (string.IsNullOrEmpty(message))
|
if (string.IsNullOrEmpty(message))
|
||||||
{
|
{
|
||||||
// Access ConsoleWindow and its data
|
// Access ConsoleWindow and its data
|
||||||
Type consoleWindowType = Type.GetType(
|
Type consoleWindowType = Type.GetType("UnityEditor.ConsoleWindow,UnityEditor");
|
||||||
"UnityEditor.ConsoleWindow,UnityEditor"
|
|
||||||
);
|
|
||||||
if (consoleWindowType != null)
|
if (consoleWindowType != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get Console window instance
|
// Get Console window instance
|
||||||
var getWindowMethod =
|
var getWindowMethod = consoleWindowType.GetMethod("GetWindow",
|
||||||
consoleWindowType.GetMethod(
|
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
|
||||||
"GetWindow",
|
null, new[] { typeof(bool) }, null) ??
|
||||||
BindingFlags.Static
|
consoleWindowType.GetMethod("GetConsoleWindow",
|
||||||
| BindingFlags.Public
|
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
| BindingFlags.NonPublic,
|
|
||||||
null,
|
|
||||||
new[] { typeof(bool) },
|
|
||||||
null
|
|
||||||
)
|
|
||||||
?? consoleWindowType.GetMethod(
|
|
||||||
"GetConsoleWindow",
|
|
||||||
BindingFlags.Static
|
|
||||||
| BindingFlags.Public
|
|
||||||
| BindingFlags.NonPublic
|
|
||||||
);
|
|
||||||
|
|
||||||
if (getWindowMethod != null)
|
if (getWindowMethod != null)
|
||||||
{
|
{
|
||||||
object consoleWindow = getWindowMethod.Invoke(
|
object consoleWindow = getWindowMethod.Invoke(null,
|
||||||
null,
|
getWindowMethod.GetParameters().Length > 0 ? new object[] { false } : null);
|
||||||
getWindowMethod.GetParameters().Length > 0
|
|
||||||
? new object[] { false }
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
if (consoleWindow != null)
|
if (consoleWindow != null)
|
||||||
{
|
{
|
||||||
// Try to find log entries collection
|
// Try to find log entries collection
|
||||||
foreach (
|
foreach (var prop in consoleWindowType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
|
||||||
var prop in consoleWindowType.GetProperties(
|
|
||||||
BindingFlags.Instance
|
|
||||||
| BindingFlags.Public
|
|
||||||
| BindingFlags.NonPublic
|
|
||||||
)
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
if (
|
if (prop.PropertyType.IsArray ||
|
||||||
prop.PropertyType.IsArray
|
(prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)))
|
||||||
|| (
|
|
||||||
prop.PropertyType.IsGenericType
|
|
||||||
&& prop.PropertyType.GetGenericTypeDefinition()
|
|
||||||
== typeof(List<>)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var logItems = prop.GetValue(consoleWindow);
|
var logItems = prop.GetValue(consoleWindow);
|
||||||
if (logItems != null)
|
if (logItems != null)
|
||||||
{
|
{
|
||||||
if (
|
if (logItems.GetType().IsArray && i < ((Array)logItems).Length)
|
||||||
logItems.GetType().IsArray
|
|
||||||
&& i < ((Array)logItems).Length
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var entry = (
|
var entry = ((Array)logItems).GetValue(i);
|
||||||
(Array)logItems
|
|
||||||
).GetValue(i);
|
|
||||||
if (entry != null)
|
if (entry != null)
|
||||||
{
|
{
|
||||||
var entryType = entry.GetType();
|
var entryType = entry.GetType();
|
||||||
var entryMessageProp =
|
var entryMessageProp = entryType.GetProperty("message") ??
|
||||||
entryType.GetProperty(
|
entryType.GetProperty("Message");
|
||||||
"message"
|
|
||||||
)
|
|
||||||
?? entryType.GetProperty(
|
|
||||||
"Message"
|
|
||||||
);
|
|
||||||
if (entryMessageProp != null)
|
if (entryMessageProp != null)
|
||||||
{
|
{
|
||||||
object value =
|
object value = entryMessageProp.GetValue(entry);
|
||||||
entryMessageProp.GetValue(
|
|
||||||
entry
|
|
||||||
);
|
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
message =
|
message = value.ToString();
|
||||||
value.ToString();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -405,42 +309,23 @@ public static class EditorControlHandler
|
||||||
if (Application.platform == RuntimePlatform.WindowsEditor)
|
if (Application.platform == RuntimePlatform.WindowsEditor)
|
||||||
{
|
{
|
||||||
logPath = System.IO.Path.Combine(
|
logPath = System.IO.Path.Combine(
|
||||||
Environment.GetFolderPath(
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
Environment.SpecialFolder.LocalApplicationData
|
"Unity", "Editor", "Editor.log");
|
||||||
),
|
|
||||||
"Unity",
|
|
||||||
"Editor",
|
|
||||||
"Editor.log"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
else if (Application.platform == RuntimePlatform.OSXEditor)
|
else if (Application.platform == RuntimePlatform.OSXEditor)
|
||||||
{
|
{
|
||||||
logPath = System.IO.Path.Combine(
|
logPath = System.IO.Path.Combine(
|
||||||
Environment.GetFolderPath(
|
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
|
||||||
Environment.SpecialFolder.Personal
|
"Library", "Logs", "Unity", "Editor.log");
|
||||||
),
|
|
||||||
"Library",
|
|
||||||
"Logs",
|
|
||||||
"Unity",
|
|
||||||
"Editor.log"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
else if (Application.platform == RuntimePlatform.LinuxEditor)
|
else if (Application.platform == RuntimePlatform.LinuxEditor)
|
||||||
{
|
{
|
||||||
logPath = System.IO.Path.Combine(
|
logPath = System.IO.Path.Combine(
|
||||||
Environment.GetFolderPath(
|
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
|
||||||
Environment.SpecialFolder.Personal
|
".config", "unity3d", "logs", "Editor.log");
|
||||||
),
|
|
||||||
".config",
|
|
||||||
"unity3d",
|
|
||||||
"logs",
|
|
||||||
"Editor.log"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!string.IsNullOrEmpty(logPath) && System.IO.File.Exists(logPath))
|
||||||
!string.IsNullOrEmpty(logPath) && System.IO.File.Exists(logPath)
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
// Read last few lines from the log file
|
// Read last few lines from the log file
|
||||||
var logLines = ReadLastLines(logPath, 100);
|
var logLines = ReadLastLines(logPath, 100);
|
||||||
|
|
@ -461,17 +346,14 @@ public static class EditorControlHandler
|
||||||
string stackTrace = "";
|
string stackTrace = "";
|
||||||
if (getStackTraceMethod != null)
|
if (getStackTraceMethod != null)
|
||||||
{
|
{
|
||||||
stackTrace =
|
stackTrace = getStackTraceMethod.Invoke(null, new object[] { i })?.ToString() ?? "";
|
||||||
getStackTraceMethod.Invoke(null, new object[] { i })?.ToString() ?? "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by type
|
// Filter by type
|
||||||
bool typeMatch =
|
bool typeMatch = (logType == 0 && showLogs) ||
|
||||||
(logType == 0 && showLogs)
|
(logType == 1 && showWarnings) ||
|
||||||
|| (logType == 1 && showWarnings)
|
(logType == 2 && showErrors);
|
||||||
|| (logType == 2 && showErrors);
|
if (!typeMatch) continue;
|
||||||
if (!typeMatch)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Filter by search term
|
// Filter by search term
|
||||||
bool searchMatch = true;
|
bool searchMatch = true;
|
||||||
|
|
@ -489,24 +371,16 @@ public static class EditorControlHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!searchMatch)
|
if (!searchMatch) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
// Add matching entry to results
|
// Add matching entry to results
|
||||||
string typeStr =
|
string typeStr = logType == 0 ? "Log" : logType == 1 ? "Warning" : "Error";
|
||||||
logType == 0
|
entries.Add(new
|
||||||
? "Log"
|
|
||||||
: logType == 1
|
|
||||||
? "Warning"
|
|
||||||
: "Error";
|
|
||||||
entries.Add(
|
|
||||||
new
|
|
||||||
{
|
{
|
||||||
type = typeStr,
|
type = typeStr,
|
||||||
message = message,
|
message,
|
||||||
stackTrace = stackTrace
|
stackTrace
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
|
|
@ -519,7 +393,7 @@ public static class EditorControlHandler
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
message = "Console logs retrieved successfully",
|
message = "Console logs retrieved successfully",
|
||||||
entries = entries,
|
entries,
|
||||||
total_entries = count,
|
total_entries = count,
|
||||||
filtered_count = entries.Count,
|
filtered_count = entries.Count,
|
||||||
show_logs = showLogs,
|
show_logs = showLogs,
|
||||||
|
|
@ -541,10 +415,8 @@ public static class EditorControlHandler
|
||||||
{
|
{
|
||||||
foreach (var methodName in methodNames)
|
foreach (var methodName in methodNames)
|
||||||
{
|
{
|
||||||
var method = type.GetMethod(
|
var method = type.GetMethod(methodName,
|
||||||
methodName,
|
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
|
|
||||||
);
|
|
||||||
if (method != null)
|
if (method != null)
|
||||||
return method;
|
return method;
|
||||||
}
|
}
|
||||||
|
|
@ -556,34 +428,20 @@ public static class EditorControlHandler
|
||||||
BuildTarget target;
|
BuildTarget target;
|
||||||
switch (platform.ToLower())
|
switch (platform.ToLower())
|
||||||
{
|
{
|
||||||
case "windows":
|
case "windows": target = BuildTarget.StandaloneWindows64; break;
|
||||||
target = BuildTarget.StandaloneWindows64;
|
case "mac": target = BuildTarget.StandaloneOSX; break;
|
||||||
break;
|
case "linux": target = BuildTarget.StandaloneLinux64; break;
|
||||||
case "mac":
|
case "android": target = BuildTarget.Android; break;
|
||||||
target = BuildTarget.StandaloneOSX;
|
case "ios": target = BuildTarget.iOS; break;
|
||||||
break;
|
case "webgl": target = BuildTarget.WebGL; break;
|
||||||
case "linux":
|
default: target = (BuildTarget)(-1); break; // Invalid target
|
||||||
target = BuildTarget.StandaloneLinux64;
|
|
||||||
break;
|
|
||||||
case "android":
|
|
||||||
target = BuildTarget.Android;
|
|
||||||
break;
|
|
||||||
case "ios":
|
|
||||||
target = BuildTarget.iOS;
|
|
||||||
break;
|
|
||||||
case "webgl":
|
|
||||||
target = BuildTarget.WebGL;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
target = (BuildTarget)(-1);
|
|
||||||
break; // Invalid target
|
|
||||||
}
|
}
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string[] GetEnabledScenes()
|
private static string[] GetEnabledScenes()
|
||||||
{
|
{
|
||||||
var scenes = new System.Collections.Generic.List<string>();
|
var scenes = new List<string>();
|
||||||
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
|
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
|
||||||
{
|
{
|
||||||
if (EditorBuildSettings.scenes[i].enabled)
|
if (EditorBuildSettings.scenes[i].enabled)
|
||||||
|
|
@ -602,12 +460,8 @@ public static class EditorControlHandler
|
||||||
var result = new Dictionary<string, object>();
|
var result = new Dictionary<string, object>();
|
||||||
|
|
||||||
// Get all public and non-public properties
|
// Get all public and non-public properties
|
||||||
var properties = type.GetProperties(
|
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic |
|
||||||
BindingFlags.Public
|
BindingFlags.Static | BindingFlags.Instance);
|
||||||
| BindingFlags.NonPublic
|
|
||||||
| BindingFlags.Static
|
|
||||||
| BindingFlags.Instance
|
|
||||||
);
|
|
||||||
var propList = new List<string>();
|
var propList = new List<string>();
|
||||||
foreach (var prop in properties)
|
foreach (var prop in properties)
|
||||||
{
|
{
|
||||||
|
|
@ -616,12 +470,8 @@ public static class EditorControlHandler
|
||||||
result["Properties"] = propList;
|
result["Properties"] = propList;
|
||||||
|
|
||||||
// Get all public and non-public fields
|
// Get all public and non-public fields
|
||||||
var fields = type.GetFields(
|
var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic |
|
||||||
BindingFlags.Public
|
BindingFlags.Static | BindingFlags.Instance);
|
||||||
| BindingFlags.NonPublic
|
|
||||||
| BindingFlags.Static
|
|
||||||
| BindingFlags.Instance
|
|
||||||
);
|
|
||||||
var fieldList = new List<string>();
|
var fieldList = new List<string>();
|
||||||
foreach (var field in fields)
|
foreach (var field in fields)
|
||||||
{
|
{
|
||||||
|
|
@ -630,21 +480,15 @@ public static class EditorControlHandler
|
||||||
result["Fields"] = fieldList;
|
result["Fields"] = fieldList;
|
||||||
|
|
||||||
// Get all public and non-public methods
|
// Get all public and non-public methods
|
||||||
var methods = type.GetMethods(
|
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic |
|
||||||
BindingFlags.Public
|
BindingFlags.Static | BindingFlags.Instance);
|
||||||
| BindingFlags.NonPublic
|
|
||||||
| BindingFlags.Static
|
|
||||||
| BindingFlags.Instance
|
|
||||||
);
|
|
||||||
var methodList = new List<string>();
|
var methodList = new List<string>();
|
||||||
foreach (var method in methods)
|
foreach (var method in methods)
|
||||||
{
|
{
|
||||||
if (!method.Name.StartsWith("get_") && !method.Name.StartsWith("set_"))
|
if (!method.Name.StartsWith("get_") && !method.Name.StartsWith("set_"))
|
||||||
{
|
{
|
||||||
var parameters = string.Join(
|
var parameters = string.Join(", ", method.GetParameters()
|
||||||
", ",
|
.Select(p => $"{p.ParameterType.Name} {p.Name}"));
|
||||||
method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}")
|
|
||||||
);
|
|
||||||
methodList.Add($"{method.ReturnType.Name} {method.Name}({parameters})");
|
methodList.Add($"{method.ReturnType.Name} {method.Name}({parameters})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -658,16 +502,13 @@ public static class EditorControlHandler
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static Dictionary<string, string> GetObjectValues(object obj)
|
private static Dictionary<string, string> GetObjectValues(object obj)
|
||||||
{
|
{
|
||||||
if (obj == null)
|
if (obj == null) return new Dictionary<string, string>();
|
||||||
return new Dictionary<string, string>();
|
|
||||||
|
|
||||||
var result = new Dictionary<string, string>();
|
var result = new Dictionary<string, string>();
|
||||||
var type = obj.GetType();
|
var type = obj.GetType();
|
||||||
|
|
||||||
// Get all property values
|
// Get all property values
|
||||||
var properties = type.GetProperties(
|
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance
|
|
||||||
);
|
|
||||||
foreach (var prop in properties)
|
foreach (var prop in properties)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -682,9 +523,7 @@ public static class EditorControlHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all field values
|
// Get all field values
|
||||||
var fields = type.GetFields(
|
var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance
|
|
||||||
);
|
|
||||||
foreach (var field in fields)
|
foreach (var field in fields)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -708,14 +547,7 @@ public static class EditorControlHandler
|
||||||
{
|
{
|
||||||
var result = new List<string>();
|
var result = new List<string>();
|
||||||
|
|
||||||
using (
|
using (var stream = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite))
|
||||||
var stream = new System.IO.FileStream(
|
|
||||||
filePath,
|
|
||||||
System.IO.FileMode.Open,
|
|
||||||
System.IO.FileAccess.Read,
|
|
||||||
System.IO.FileShare.ReadWrite
|
|
||||||
)
|
|
||||||
)
|
|
||||||
using (var reader = new System.IO.StreamReader(stream))
|
using (var reader = new System.IO.StreamReader(stream))
|
||||||
{
|
{
|
||||||
string line;
|
string line;
|
||||||
|
|
@ -753,29 +585,24 @@ public static class EditorControlHandler
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a list of available editor commands that can be executed
|
/// Gets a list of available editor commands that can be executed
|
||||||
/// the method should have a MenuItem attribute
|
/// the method should have a MenuItem attribute
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Object containing list of available command paths</returns>
|
/// <returns>Object containing list of available command paths</returns>
|
||||||
[MenuItem("Window/Get Available Commands")]
|
|
||||||
private static object GetAvailableCommands()
|
private static object GetAvailableCommands()
|
||||||
{
|
{
|
||||||
var commands = new List<string>();
|
var commands = new List<string>();
|
||||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||||
foreach (Type type in assembly.GetTypes())
|
foreach (Type type in assembly.GetTypes())
|
||||||
{
|
{
|
||||||
MethodInfo[] methods = type.GetMethods(
|
MethodInfo[] methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach (MethodInfo method in methods)
|
foreach (MethodInfo method in methods)
|
||||||
{
|
{
|
||||||
// Look for the MenuItem attribute
|
// Look for the MenuItem attribute
|
||||||
object[] attributes = method.GetCustomAttributes(
|
object[] attributes = method.GetCustomAttributes(typeof(UnityEditor.MenuItem), false);
|
||||||
typeof(UnityEditor.MenuItem),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
if (attributes.Length > 0)
|
if (attributes.Length > 0)
|
||||||
{
|
{
|
||||||
UnityEditor.MenuItem menuItem = attributes[0] as UnityEditor.MenuItem;
|
UnityEditor.MenuItem menuItem = attributes[0] as UnityEditor.MenuItem;
|
||||||
|
|
@ -783,11 +610,7 @@ public static class EditorControlHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Debug.Log($"commands.Count: {commands.Count}");
|
|
||||||
foreach (var command in commands)
|
|
||||||
{
|
|
||||||
Debug.Log($"Command: {command}");
|
|
||||||
}
|
|
||||||
return new { commands = commands };
|
return new { commands = commands };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,8 +2,10 @@ using UnityEngine;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using UnityEngine.Rendering.Universal;
|
using UnityEngine.Rendering.Universal;
|
||||||
using UnityEngine.Rendering;
|
using UnityEngine.Rendering;
|
||||||
|
using UnityEditor;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace MCPServer.Editor.Commands
|
namespace UnityMCP.Editor.Commands
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles material-related commands
|
/// Handles material-related commands
|
||||||
|
|
@ -22,26 +24,72 @@ namespace MCPServer.Editor.Commands
|
||||||
// Check if URP is being used
|
// Check if URP is being used
|
||||||
bool isURP = GraphicsSettings.currentRenderPipeline is UniversalRenderPipelineAsset;
|
bool isURP = GraphicsSettings.currentRenderPipeline is UniversalRenderPipelineAsset;
|
||||||
|
|
||||||
// Create material with appropriate shader based on render pipeline
|
Material material = null;
|
||||||
Material material;
|
string materialName = (string)@params["material_name"];
|
||||||
if (isURP)
|
bool createIfMissing = (bool)(@params["create_if_missing"] ?? true);
|
||||||
|
string materialPath = null;
|
||||||
|
|
||||||
|
// If material name is specified, try to find or create it
|
||||||
|
if (!string.IsNullOrEmpty(materialName))
|
||||||
{
|
{
|
||||||
material = new Material(Shader.Find("Universal Render Pipeline/Lit"));
|
// Ensure Materials folder exists
|
||||||
|
const string materialsFolder = "Assets/Materials";
|
||||||
|
if (!Directory.Exists(materialsFolder))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(materialsFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
materialPath = $"{materialsFolder}/{materialName}.mat";
|
||||||
|
material = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
|
||||||
|
|
||||||
|
if (material == null && createIfMissing)
|
||||||
|
{
|
||||||
|
// Create new material with appropriate shader
|
||||||
|
material = new Material(isURP ? Shader.Find("Universal Render Pipeline/Lit") : Shader.Find("Standard"));
|
||||||
|
material.name = materialName;
|
||||||
|
|
||||||
|
// Save the material asset
|
||||||
|
AssetDatabase.CreateAsset(material, materialPath);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
}
|
||||||
|
else if (material == null)
|
||||||
|
{
|
||||||
|
throw new System.Exception($"Material '{materialName}' not found and create_if_missing is false.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
material = new Material(Shader.Find("Standard"));
|
// Create a temporary material if no name specified
|
||||||
|
material = new Material(isURP ? Shader.Find("Universal Render Pipeline/Lit") : Shader.Find("Standard"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@params.ContainsKey("material_name")) material.name = (string)@params["material_name"];
|
// Apply color if specified
|
||||||
if (@params.ContainsKey("color"))
|
if (@params.ContainsKey("color"))
|
||||||
{
|
{
|
||||||
var colorArray = (JArray)@params["color"] ?? throw new System.Exception("Invalid color parameter.");
|
var colorArray = (JArray)@params["color"];
|
||||||
if (colorArray.Count != 3) throw new System.Exception("Color must be an array of 3 floats [r, g, b].");
|
if (colorArray.Count < 3 || colorArray.Count > 4)
|
||||||
material.color = new Color((float)colorArray[0], (float)colorArray[1], (float)colorArray[2]);
|
throw new System.Exception("Color must be an array of 3 (RGB) or 4 (RGBA) floats.");
|
||||||
|
|
||||||
|
Color color = new(
|
||||||
|
(float)colorArray[0],
|
||||||
|
(float)colorArray[1],
|
||||||
|
(float)colorArray[2],
|
||||||
|
colorArray.Count > 3 ? (float)colorArray[3] : 1.0f
|
||||||
|
);
|
||||||
|
material.color = color;
|
||||||
|
|
||||||
|
// If this is a saved material, make sure to save the color change
|
||||||
|
if (!string.IsNullOrEmpty(materialPath))
|
||||||
|
{
|
||||||
|
EditorUtility.SetDirty(material);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the material to the renderer
|
||||||
renderer.material = material;
|
renderer.material = material;
|
||||||
return new { material_name = material.name };
|
|
||||||
|
return new { material_name = material.name, path = materialPath };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using MCPServer.Editor.Helpers;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine.SceneManagement;
|
using UnityEngine.SceneManagement;
|
||||||
using UnityEditor.SceneManagement;
|
using UnityEditor.SceneManagement;
|
||||||
|
using UnityMCP.Editor.Helpers;
|
||||||
|
|
||||||
namespace MCPServer.Editor.Commands
|
namespace UnityMCP.Editor.Commands
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles object-related commands
|
/// Handles object-related commands
|
||||||
|
|
@ -20,11 +20,11 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object GetObjectInfo(JObject @params)
|
public static object GetObjectInfo(JObject @params)
|
||||||
{
|
{
|
||||||
string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required.");
|
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
|
||||||
var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found.");
|
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
name = obj.name,
|
obj.name,
|
||||||
position = new[] { obj.transform.position.x, obj.transform.position.y, obj.transform.position.z },
|
position = new[] { obj.transform.position.x, obj.transform.position.y, obj.transform.position.z },
|
||||||
rotation = new[] { obj.transform.eulerAngles.x, obj.transform.eulerAngles.y, obj.transform.eulerAngles.z },
|
rotation = new[] { obj.transform.eulerAngles.x, obj.transform.eulerAngles.y, obj.transform.eulerAngles.z },
|
||||||
scale = new[] { obj.transform.localScale.x, obj.transform.localScale.y, obj.transform.localScale.z }
|
scale = new[] { obj.transform.localScale.x, obj.transform.localScale.y, obj.transform.localScale.z }
|
||||||
|
|
@ -36,7 +36,7 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object CreateObject(JObject @params)
|
public static object CreateObject(JObject @params)
|
||||||
{
|
{
|
||||||
string type = (string)@params["type"] ?? throw new System.Exception("Parameter 'type' is required.");
|
string type = (string)@params["type"] ?? throw new Exception("Parameter 'type' is required.");
|
||||||
GameObject obj = type.ToUpper() switch
|
GameObject obj = type.ToUpper() switch
|
||||||
{
|
{
|
||||||
"CUBE" => GameObject.CreatePrimitive(PrimitiveType.Cube),
|
"CUBE" => GameObject.CreatePrimitive(PrimitiveType.Cube),
|
||||||
|
|
@ -48,7 +48,7 @@ namespace MCPServer.Editor.Commands
|
||||||
"CAMERA" => new GameObject("Camera") { }.AddComponent<Camera>().gameObject,
|
"CAMERA" => new GameObject("Camera") { }.AddComponent<Camera>().gameObject,
|
||||||
"LIGHT" => new GameObject("Light") { }.AddComponent<Light>().gameObject,
|
"LIGHT" => new GameObject("Light") { }.AddComponent<Light>().gameObject,
|
||||||
"DIRECTIONAL_LIGHT" => CreateDirectionalLight(),
|
"DIRECTIONAL_LIGHT" => CreateDirectionalLight(),
|
||||||
_ => throw new System.Exception($"Unsupported object type: {type}")
|
_ => throw new Exception($"Unsupported object type: {type}")
|
||||||
};
|
};
|
||||||
|
|
||||||
if (@params.ContainsKey("name")) obj.name = (string)@params["name"];
|
if (@params.ContainsKey("name")) obj.name = (string)@params["name"];
|
||||||
|
|
@ -56,7 +56,7 @@ namespace MCPServer.Editor.Commands
|
||||||
if (@params.ContainsKey("rotation")) obj.transform.eulerAngles = Vector3Helper.ParseVector3((JArray)@params["rotation"]);
|
if (@params.ContainsKey("rotation")) obj.transform.eulerAngles = Vector3Helper.ParseVector3((JArray)@params["rotation"]);
|
||||||
if (@params.ContainsKey("scale")) obj.transform.localScale = Vector3Helper.ParseVector3((JArray)@params["scale"]);
|
if (@params.ContainsKey("scale")) obj.transform.localScale = Vector3Helper.ParseVector3((JArray)@params["scale"]);
|
||||||
|
|
||||||
return new { name = obj.name };
|
return new { obj.name };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -64,8 +64,8 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object ModifyObject(JObject @params)
|
public static object ModifyObject(JObject @params)
|
||||||
{
|
{
|
||||||
string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required.");
|
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
|
||||||
var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found.");
|
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
|
||||||
|
|
||||||
// Handle basic transform properties
|
// Handle basic transform properties
|
||||||
if (@params.ContainsKey("location")) obj.transform.position = Vector3Helper.ParseVector3((JArray)@params["location"]);
|
if (@params.ContainsKey("location")) obj.transform.position = Vector3Helper.ParseVector3((JArray)@params["location"]);
|
||||||
|
|
@ -77,7 +77,7 @@ namespace MCPServer.Editor.Commands
|
||||||
if (@params.ContainsKey("set_parent"))
|
if (@params.ContainsKey("set_parent"))
|
||||||
{
|
{
|
||||||
string parentName = (string)@params["set_parent"];
|
string parentName = (string)@params["set_parent"];
|
||||||
var parent = GameObject.Find(parentName) ?? throw new System.Exception($"Parent object '{parentName}' not found.");
|
var parent = GameObject.Find(parentName) ?? throw new Exception($"Parent object '{parentName}' not found.");
|
||||||
obj.transform.SetParent(parent.transform);
|
obj.transform.SetParent(parent.transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +109,7 @@ namespace MCPServer.Editor.Commands
|
||||||
"TextMeshProUGUI" => typeof(TMPro.TextMeshProUGUI),
|
"TextMeshProUGUI" => typeof(TMPro.TextMeshProUGUI),
|
||||||
_ => Type.GetType($"UnityEngine.{componentType}") ??
|
_ => Type.GetType($"UnityEngine.{componentType}") ??
|
||||||
Type.GetType(componentType) ??
|
Type.GetType(componentType) ??
|
||||||
throw new System.Exception($"Component type '{componentType}' not found.")
|
throw new Exception($"Component type '{componentType}' not found.")
|
||||||
};
|
};
|
||||||
obj.AddComponent(type);
|
obj.AddComponent(type);
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +119,7 @@ namespace MCPServer.Editor.Commands
|
||||||
string componentType = (string)@params["remove_component"];
|
string componentType = (string)@params["remove_component"];
|
||||||
Type type = Type.GetType($"UnityEngine.{componentType}") ??
|
Type type = Type.GetType($"UnityEngine.{componentType}") ??
|
||||||
Type.GetType(componentType) ??
|
Type.GetType(componentType) ??
|
||||||
throw new System.Exception($"Component type '{componentType}' not found.");
|
throw new Exception($"Component type '{componentType}' not found.");
|
||||||
var component = obj.GetComponent(type);
|
var component = obj.GetComponent(type);
|
||||||
if (component != null)
|
if (component != null)
|
||||||
UnityEngine.Object.DestroyImmediate(component);
|
UnityEngine.Object.DestroyImmediate(component);
|
||||||
|
|
@ -137,12 +137,12 @@ namespace MCPServer.Editor.Commands
|
||||||
if (componentType == "GameObject")
|
if (componentType == "GameObject")
|
||||||
{
|
{
|
||||||
var gameObjectProperty = typeof(GameObject).GetProperty(propertyName) ??
|
var gameObjectProperty = typeof(GameObject).GetProperty(propertyName) ??
|
||||||
throw new System.Exception($"Property '{propertyName}' not found on GameObject.");
|
throw new Exception($"Property '{propertyName}' not found on GameObject.");
|
||||||
|
|
||||||
// Convert value based on property type
|
// Convert value based on property type
|
||||||
object gameObjectValue = Convert.ChangeType(value, gameObjectProperty.PropertyType);
|
object gameObjectValue = Convert.ChangeType(value, gameObjectProperty.PropertyType);
|
||||||
gameObjectProperty.SetValue(obj, gameObjectValue);
|
gameObjectProperty.SetValue(obj, gameObjectValue);
|
||||||
return new { name = obj.name };
|
return new { obj.name };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle component properties
|
// Handle component properties
|
||||||
|
|
@ -170,21 +170,21 @@ namespace MCPServer.Editor.Commands
|
||||||
"TextMeshProUGUI" => typeof(TMPro.TextMeshProUGUI),
|
"TextMeshProUGUI" => typeof(TMPro.TextMeshProUGUI),
|
||||||
_ => Type.GetType($"UnityEngine.{componentType}") ??
|
_ => Type.GetType($"UnityEngine.{componentType}") ??
|
||||||
Type.GetType(componentType) ??
|
Type.GetType(componentType) ??
|
||||||
throw new System.Exception($"Component type '{componentType}' not found.")
|
throw new Exception($"Component type '{componentType}' not found.")
|
||||||
};
|
};
|
||||||
|
|
||||||
var component = obj.GetComponent(type) ??
|
var component = obj.GetComponent(type) ??
|
||||||
throw new System.Exception($"Component '{componentType}' not found on object '{name}'.");
|
throw new Exception($"Component '{componentType}' not found on object '{name}'.");
|
||||||
|
|
||||||
var property = type.GetProperty(propertyName) ??
|
var property = type.GetProperty(propertyName) ??
|
||||||
throw new System.Exception($"Property '{propertyName}' not found on component '{componentType}'.");
|
throw new Exception($"Property '{propertyName}' not found on component '{componentType}'.");
|
||||||
|
|
||||||
// Convert value based on property type
|
// Convert value based on property type
|
||||||
object propertyValue = Convert.ChangeType(value, property.PropertyType);
|
object propertyValue = Convert.ChangeType(value, property.PropertyType);
|
||||||
property.SetValue(component, propertyValue);
|
property.SetValue(component, propertyValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new { name = obj.name };
|
return new { obj.name };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -192,8 +192,8 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object DeleteObject(JObject @params)
|
public static object DeleteObject(JObject @params)
|
||||||
{
|
{
|
||||||
string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required.");
|
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
|
||||||
var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found.");
|
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
|
||||||
UnityEngine.Object.DestroyImmediate(obj);
|
UnityEngine.Object.DestroyImmediate(obj);
|
||||||
return new { name };
|
return new { name };
|
||||||
}
|
}
|
||||||
|
|
@ -203,8 +203,8 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object GetObjectProperties(JObject @params)
|
public static object GetObjectProperties(JObject @params)
|
||||||
{
|
{
|
||||||
string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required.");
|
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
|
||||||
var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found.");
|
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
|
||||||
|
|
||||||
var components = obj.GetComponents<Component>()
|
var components = obj.GetComponents<Component>()
|
||||||
.Select(c => new
|
.Select(c => new
|
||||||
|
|
@ -216,9 +216,9 @@ namespace MCPServer.Editor.Commands
|
||||||
|
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
name = obj.name,
|
obj.name,
|
||||||
tag = obj.tag,
|
obj.tag,
|
||||||
layer = obj.layer,
|
obj.layer,
|
||||||
active = obj.activeSelf,
|
active = obj.activeSelf,
|
||||||
transform = new
|
transform = new
|
||||||
{
|
{
|
||||||
|
|
@ -235,11 +235,11 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object GetComponentProperties(JObject @params)
|
public static object GetComponentProperties(JObject @params)
|
||||||
{
|
{
|
||||||
string objectName = (string)@params["object_name"] ?? throw new System.Exception("Parameter 'object_name' is required.");
|
string objectName = (string)@params["object_name"] ?? throw new Exception("Parameter 'object_name' is required.");
|
||||||
string componentType = (string)@params["component_type"] ?? throw new System.Exception("Parameter 'component_type' is required.");
|
string componentType = (string)@params["component_type"] ?? throw new Exception("Parameter 'component_type' is required.");
|
||||||
|
|
||||||
var obj = GameObject.Find(objectName) ?? throw new System.Exception($"Object '{objectName}' not found.");
|
var obj = GameObject.Find(objectName) ?? throw new Exception($"Object '{objectName}' not found.");
|
||||||
var component = obj.GetComponent(componentType) ?? throw new System.Exception($"Component '{componentType}' not found on object '{objectName}'.");
|
var component = obj.GetComponent(componentType) ?? throw new Exception($"Component '{componentType}' not found on object '{objectName}'.");
|
||||||
|
|
||||||
return GetComponentProperties(component);
|
return GetComponentProperties(component);
|
||||||
}
|
}
|
||||||
|
|
@ -249,12 +249,12 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object FindObjectsByName(JObject @params)
|
public static object FindObjectsByName(JObject @params)
|
||||||
{
|
{
|
||||||
string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required.");
|
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
|
||||||
var objects = GameObject.FindObjectsByType<GameObject>(FindObjectsSortMode.None)
|
var objects = GameObject.FindObjectsByType<GameObject>(FindObjectsSortMode.None)
|
||||||
.Where(o => o.name.Contains(name))
|
.Where(o => o.name.Contains(name))
|
||||||
.Select(o => new
|
.Select(o => new
|
||||||
{
|
{
|
||||||
name = o.name,
|
o.name,
|
||||||
path = GetGameObjectPath(o)
|
path = GetGameObjectPath(o)
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
@ -267,11 +267,11 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object FindObjectsByTag(JObject @params)
|
public static object FindObjectsByTag(JObject @params)
|
||||||
{
|
{
|
||||||
string tag = (string)@params["tag"] ?? throw new System.Exception("Parameter 'tag' is required.");
|
string tag = (string)@params["tag"] ?? throw new Exception("Parameter 'tag' is required.");
|
||||||
var objects = GameObject.FindGameObjectsWithTag(tag)
|
var objects = GameObject.FindGameObjectsWithTag(tag)
|
||||||
.Select(o => new
|
.Select(o => new
|
||||||
{
|
{
|
||||||
name = o.name,
|
o.name,
|
||||||
path = GetGameObjectPath(o)
|
path = GetGameObjectPath(o)
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
@ -295,11 +295,11 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object SelectObject(JObject @params)
|
public static object SelectObject(JObject @params)
|
||||||
{
|
{
|
||||||
string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required.");
|
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
|
||||||
var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found.");
|
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
|
||||||
|
|
||||||
Selection.activeGameObject = obj;
|
Selection.activeGameObject = obj;
|
||||||
return new { name = obj.name };
|
return new { obj.name };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -315,7 +315,7 @@ namespace MCPServer.Editor.Commands
|
||||||
{
|
{
|
||||||
selected = new
|
selected = new
|
||||||
{
|
{
|
||||||
name = selected.name,
|
selected.name,
|
||||||
path = GetGameObjectPath(selected)
|
path = GetGameObjectPath(selected)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -379,7 +379,7 @@ namespace MCPServer.Editor.Commands
|
||||||
{
|
{
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
name = obj.name,
|
obj.name,
|
||||||
children = Enumerable.Range(0, obj.transform.childCount)
|
children = Enumerable.Range(0, obj.transform.childCount)
|
||||||
.Select(i => BuildHierarchyNode(obj.transform.GetChild(i).gameObject))
|
.Select(i => BuildHierarchyNode(obj.transform.GetChild(i).gameObject))
|
||||||
.ToList()
|
.ToList()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.SceneManagement;
|
using UnityEngine.SceneManagement;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditor.SceneManagement;
|
using UnityEditor.SceneManagement;
|
||||||
|
|
||||||
namespace MCPServer.Editor.Commands
|
namespace UnityMCP.Editor.Commands
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles scene-related commands for the MCP Server
|
/// Handles scene-related commands for the MCP Server
|
||||||
|
|
@ -42,7 +42,7 @@ namespace MCPServer.Editor.Commands
|
||||||
EditorSceneManager.OpenScene(scenePath);
|
EditorSceneManager.OpenScene(scenePath);
|
||||||
return new { success = true, message = $"Opened scene: {scenePath}" };
|
return new { success = true, message = $"Opened scene: {scenePath}" };
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return new { success = false, error = $"Failed to open scene: {e.Message}", stackTrace = e.StackTrace };
|
return new { success = false, error = $"Failed to open scene: {e.Message}", stackTrace = e.StackTrace };
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +60,7 @@ namespace MCPServer.Editor.Commands
|
||||||
EditorSceneManager.SaveScene(scene);
|
EditorSceneManager.SaveScene(scene);
|
||||||
return new { success = true, message = $"Saved scene: {scene.path}" };
|
return new { success = true, message = $"Saved scene: {scene.path}" };
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return new { success = false, error = $"Failed to save scene: {e.Message}", stackTrace = e.StackTrace };
|
return new { success = false, error = $"Failed to save scene: {e.Message}", stackTrace = e.StackTrace };
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +96,7 @@ namespace MCPServer.Editor.Commands
|
||||||
|
|
||||||
return new { success = true, message = $"Created new scene at: {scenePath}" };
|
return new { success = true, message = $"Created new scene at: {scenePath}" };
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return new { success = false, error = $"Failed to create new scene: {e.Message}", stackTrace = e.StackTrace };
|
return new { success = false, error = $"Failed to create new scene: {e.Message}", stackTrace = e.StackTrace };
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +131,7 @@ namespace MCPServer.Editor.Commands
|
||||||
EditorSceneManager.OpenScene(scenePath);
|
EditorSceneManager.OpenScene(scenePath);
|
||||||
return new { success = true, message = $"Changed to scene: {scenePath}" };
|
return new { success = true, message = $"Changed to scene: {scenePath}" };
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return new { success = false, error = $"Failed to change scene: {e.Message}", stackTrace = e.StackTrace };
|
return new { success = false, error = $"Failed to change scene: {e.Message}", stackTrace = e.StackTrace };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,8 @@ using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using MCPServer.Editor.Helpers;
|
|
||||||
|
|
||||||
namespace MCPServer.Editor.Commands
|
namespace UnityMCP.Editor.Commands
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles script-related commands for Unity
|
/// Handles script-related commands for Unity
|
||||||
|
|
@ -19,12 +18,9 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object ViewScript(JObject @params)
|
public static object ViewScript(JObject @params)
|
||||||
{
|
{
|
||||||
string scriptPath = (string)@params["script_path"] ?? throw new System.Exception("Parameter 'script_path' is required.");
|
string scriptPath = (string)@params["script_path"] ?? throw new Exception("Parameter 'script_path' is required.");
|
||||||
bool requireExists = (bool?)@params["require_exists"] ?? true;
|
bool requireExists = (bool?)@params["require_exists"] ?? true;
|
||||||
|
|
||||||
// Debug to help diagnose issues
|
|
||||||
Debug.Log($"ViewScript - Original script path: {scriptPath}");
|
|
||||||
|
|
||||||
// Handle path correctly to avoid double "Assets" folder issue
|
// Handle path correctly to avoid double "Assets" folder issue
|
||||||
string relativePath;
|
string relativePath;
|
||||||
if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|
@ -38,14 +34,12 @@ namespace MCPServer.Editor.Commands
|
||||||
}
|
}
|
||||||
|
|
||||||
string fullPath = Path.Combine(Application.dataPath, relativePath);
|
string fullPath = Path.Combine(Application.dataPath, relativePath);
|
||||||
Debug.Log($"ViewScript - Relative path: {relativePath}");
|
|
||||||
Debug.Log($"ViewScript - Full path: {fullPath}");
|
|
||||||
|
|
||||||
if (!File.Exists(fullPath))
|
if (!File.Exists(fullPath))
|
||||||
{
|
{
|
||||||
if (requireExists)
|
if (requireExists)
|
||||||
{
|
{
|
||||||
throw new System.Exception($"Script file not found: {scriptPath}");
|
throw new Exception($"Script file not found: {scriptPath}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -76,7 +70,7 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object CreateScript(JObject @params)
|
public static object CreateScript(JObject @params)
|
||||||
{
|
{
|
||||||
string scriptName = (string)@params["script_name"] ?? throw new System.Exception("Parameter 'script_name' is required.");
|
string scriptName = (string)@params["script_name"] ?? throw new Exception("Parameter 'script_name' is required.");
|
||||||
string scriptType = (string)@params["script_type"] ?? "MonoBehaviour";
|
string scriptType = (string)@params["script_type"] ?? "MonoBehaviour";
|
||||||
string namespaceName = (string)@params["namespace"];
|
string namespaceName = (string)@params["namespace"];
|
||||||
string template = (string)@params["template"];
|
string template = (string)@params["template"];
|
||||||
|
|
@ -115,9 +109,6 @@ namespace MCPServer.Editor.Commands
|
||||||
|
|
||||||
// Create the full directory path, avoiding Assets/Assets issue
|
// Create the full directory path, avoiding Assets/Assets issue
|
||||||
string folderPath = Path.Combine(Application.dataPath, scriptPath);
|
string folderPath = Path.Combine(Application.dataPath, scriptPath);
|
||||||
Debug.Log($"CreateScript - Script name: {scriptName}");
|
|
||||||
Debug.Log($"CreateScript - Script path: {scriptPath}");
|
|
||||||
Debug.Log($"CreateScript - Creating script in folder path: {folderPath}");
|
|
||||||
|
|
||||||
// Create directory if it doesn't exist
|
// Create directory if it doesn't exist
|
||||||
if (!Directory.Exists(folderPath))
|
if (!Directory.Exists(folderPath))
|
||||||
|
|
@ -129,7 +120,7 @@ namespace MCPServer.Editor.Commands
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new System.Exception($"Failed to create directory '{scriptPath}': {ex.Message}");
|
throw new Exception($"Failed to create directory '{scriptPath}': {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,7 +128,7 @@ namespace MCPServer.Editor.Commands
|
||||||
string fullFilePath = Path.Combine(folderPath, scriptName);
|
string fullFilePath = Path.Combine(folderPath, scriptName);
|
||||||
if (File.Exists(fullFilePath) && !overwrite)
|
if (File.Exists(fullFilePath) && !overwrite)
|
||||||
{
|
{
|
||||||
throw new System.Exception($"Script file '{scriptName}' already exists in '{scriptPath}' and overwrite is not enabled.");
|
throw new Exception($"Script file '{scriptName}' already exists in '{scriptPath}' and overwrite is not enabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -151,7 +142,7 @@ namespace MCPServer.Editor.Commands
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Otherwise generate content based on template and parameters
|
// Otherwise generate content based on template and parameters
|
||||||
StringBuilder contentBuilder = new StringBuilder();
|
StringBuilder contentBuilder = new();
|
||||||
|
|
||||||
// Add using directives
|
// Add using directives
|
||||||
contentBuilder.AppendLine("using UnityEngine;");
|
contentBuilder.AppendLine("using UnityEngine;");
|
||||||
|
|
@ -213,7 +204,8 @@ namespace MCPServer.Editor.Commands
|
||||||
relativePath = $"Assets/{relativePath}";
|
relativePath = $"Assets/{relativePath}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return new {
|
return new
|
||||||
|
{
|
||||||
message = $"Created script: {Path.Combine(relativePath, scriptName).Replace('\\', '/')}",
|
message = $"Created script: {Path.Combine(relativePath, scriptName).Replace('\\', '/')}",
|
||||||
script_path = Path.Combine(relativePath, scriptName).Replace('\\', '/')
|
script_path = Path.Combine(relativePath, scriptName).Replace('\\', '/')
|
||||||
};
|
};
|
||||||
|
|
@ -221,7 +213,7 @@ namespace MCPServer.Editor.Commands
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogError($"Failed to create script: {ex.Message}\n{ex.StackTrace}");
|
Debug.LogError($"Failed to create script: {ex.Message}\n{ex.StackTrace}");
|
||||||
throw new System.Exception($"Failed to create script '{scriptName}': {ex.Message}");
|
throw new Exception($"Failed to create script '{scriptName}': {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,8 +222,8 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object UpdateScript(JObject @params)
|
public static object UpdateScript(JObject @params)
|
||||||
{
|
{
|
||||||
string scriptPath = (string)@params["script_path"] ?? throw new System.Exception("Parameter 'script_path' is required.");
|
string scriptPath = (string)@params["script_path"] ?? throw new Exception("Parameter 'script_path' is required.");
|
||||||
string content = (string)@params["content"] ?? throw new System.Exception("Parameter 'content' is required.");
|
string content = (string)@params["content"] ?? throw new Exception("Parameter 'content' is required.");
|
||||||
bool createIfMissing = (bool?)@params["create_if_missing"] ?? false;
|
bool createIfMissing = (bool?)@params["create_if_missing"] ?? false;
|
||||||
bool createFolderIfMissing = (bool?)@params["create_folder_if_missing"] ?? false;
|
bool createFolderIfMissing = (bool?)@params["create_folder_if_missing"] ?? false;
|
||||||
|
|
||||||
|
|
@ -251,9 +243,7 @@ namespace MCPServer.Editor.Commands
|
||||||
string directory = Path.GetDirectoryName(fullPath);
|
string directory = Path.GetDirectoryName(fullPath);
|
||||||
|
|
||||||
// Debug the paths to help diagnose issues
|
// Debug the paths to help diagnose issues
|
||||||
Debug.Log($"UpdateScript - Original script path: {scriptPath}");
|
|
||||||
Debug.Log($"UpdateScript - Relative path: {relativePath}");
|
|
||||||
Debug.Log($"UpdateScript - Full path: {fullPath}");
|
|
||||||
|
|
||||||
// Check if file exists, create if requested
|
// Check if file exists, create if requested
|
||||||
if (!File.Exists(fullPath))
|
if (!File.Exists(fullPath))
|
||||||
|
|
@ -267,7 +257,7 @@ namespace MCPServer.Editor.Commands
|
||||||
}
|
}
|
||||||
else if (!Directory.Exists(directory))
|
else if (!Directory.Exists(directory))
|
||||||
{
|
{
|
||||||
throw new System.Exception($"Directory does not exist: {Path.GetDirectoryName(scriptPath)}");
|
throw new Exception($"Directory does not exist: {Path.GetDirectoryName(scriptPath)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the file with content
|
// Create the file with content
|
||||||
|
|
@ -277,7 +267,7 @@ namespace MCPServer.Editor.Commands
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new System.Exception($"Script file not found: {scriptPath}");
|
throw new Exception($"Script file not found: {scriptPath}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,7 +306,7 @@ namespace MCPServer.Editor.Commands
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Directory.Exists(fullPath))
|
if (!Directory.Exists(fullPath))
|
||||||
throw new System.Exception($"Folder not found: {folderPath}");
|
throw new Exception($"Folder not found: {folderPath}");
|
||||||
|
|
||||||
string[] scripts = Directory.GetFiles(fullPath, "*.cs", SearchOption.AllDirectories)
|
string[] scripts = Directory.GetFiles(fullPath, "*.cs", SearchOption.AllDirectories)
|
||||||
.Select(path => path.Replace(Application.dataPath, "Assets"))
|
.Select(path => path.Replace(Application.dataPath, "Assets"))
|
||||||
|
|
@ -330,14 +320,14 @@ namespace MCPServer.Editor.Commands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object AttachScript(JObject @params)
|
public static object AttachScript(JObject @params)
|
||||||
{
|
{
|
||||||
string objectName = (string)@params["object_name"] ?? throw new System.Exception("Parameter 'object_name' is required.");
|
string objectName = (string)@params["object_name"] ?? throw new Exception("Parameter 'object_name' is required.");
|
||||||
string scriptName = (string)@params["script_name"] ?? throw new System.Exception("Parameter 'script_name' is required.");
|
string scriptName = (string)@params["script_name"] ?? throw new Exception("Parameter 'script_name' is required.");
|
||||||
string scriptPath = (string)@params["script_path"]; // Optional
|
string scriptPath = (string)@params["script_path"]; // Optional
|
||||||
|
|
||||||
// Find the target object
|
// Find the target object
|
||||||
GameObject targetObject = GameObject.Find(objectName);
|
GameObject targetObject = GameObject.Find(objectName);
|
||||||
if (targetObject == null)
|
if (targetObject == null)
|
||||||
throw new System.Exception($"Object '{objectName}' not found in scene.");
|
throw new Exception($"Object '{objectName}' not found in scene.");
|
||||||
|
|
||||||
// Ensure script name ends with .cs
|
// Ensure script name ends with .cs
|
||||||
if (!scriptName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
|
if (!scriptName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|
@ -359,7 +349,7 @@ namespace MCPServer.Editor.Commands
|
||||||
MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptPath);
|
MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptPath);
|
||||||
if (scriptAsset != null)
|
if (scriptAsset != null)
|
||||||
{
|
{
|
||||||
System.Type scriptType = scriptAsset.GetClass();
|
Type scriptType = scriptAsset.GetClass();
|
||||||
if (scriptType != null)
|
if (scriptType != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -378,7 +368,7 @@ namespace MCPServer.Editor.Commands
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogError($"Error attaching script component: {ex.Message}");
|
Debug.LogError($"Error attaching script component: {ex.Message}");
|
||||||
throw new System.Exception($"Failed to add component: {ex.Message}");
|
throw new Exception($"Failed to add component: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -394,7 +384,7 @@ namespace MCPServer.Editor.Commands
|
||||||
guids = AssetDatabase.FindAssets(scriptNameWithoutExtension);
|
guids = AssetDatabase.FindAssets(scriptNameWithoutExtension);
|
||||||
|
|
||||||
if (guids.Length == 0)
|
if (guids.Length == 0)
|
||||||
throw new System.Exception($"Script '{scriptFileName}' not found in project.");
|
throw new Exception($"Script '{scriptFileName}' not found in project.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check each potential script until we find one that can be attached
|
// Check each potential script until we find one that can be attached
|
||||||
|
|
@ -416,7 +406,7 @@ namespace MCPServer.Editor.Commands
|
||||||
if (scriptAsset == null)
|
if (scriptAsset == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
System.Type scriptType = scriptAsset.GetClass();
|
Type scriptType = scriptAsset.GetClass();
|
||||||
if (scriptType == null || !typeof(MonoBehaviour).IsAssignableFrom(scriptType))
|
if (scriptType == null || !typeof(MonoBehaviour).IsAssignableFrom(scriptType))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
@ -452,7 +442,7 @@ namespace MCPServer.Editor.Commands
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've tried all possibilities and nothing worked
|
// If we've tried all possibilities and nothing worked
|
||||||
throw new System.Exception($"Could not attach script '{scriptFileName}' to object '{objectName}'. No valid script found or component creation failed.");
|
throw new Exception($"Could not attach script '{scriptFileName}' to object '{objectName}'. No valid script found or component creation failed.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e59036660cc33d24596fbbf6d4657a83
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
using UnityMCP.Editor.Models;
|
||||||
|
|
||||||
|
namespace UnityMCP.Editor.Data
|
||||||
|
{
|
||||||
|
public class DefaultServerConfig : ServerConfig
|
||||||
|
{
|
||||||
|
public new string unityHost = "localhost";
|
||||||
|
public new int unityPort = 6400;
|
||||||
|
public new int mcpPort = 6500;
|
||||||
|
public new float connectionTimeout = 15.0f;
|
||||||
|
public new int bufferSize = 32768;
|
||||||
|
public new string logLevel = "INFO";
|
||||||
|
public new string logFormat = "%(asctime)s - %(name)s - %(levelname)s - %(message)s";
|
||||||
|
public new int maxRetries = 3;
|
||||||
|
public new float retryDelay = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: de8f5721c34f7194392e9d8c7d0226c0
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using UnityMCP.Editor.Models;
|
||||||
|
|
||||||
|
namespace UnityMCP.Editor.Data
|
||||||
|
{
|
||||||
|
public class McpClients
|
||||||
|
{
|
||||||
|
public List<McpClient> clients = new() {
|
||||||
|
new() {
|
||||||
|
name = "Claude Desktop",
|
||||||
|
windowsConfigPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"Claude",
|
||||||
|
"claude_desktop_config.json"
|
||||||
|
),
|
||||||
|
linuxConfigPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"Claude",
|
||||||
|
"claude_desktop_config.json"
|
||||||
|
),
|
||||||
|
mcpType = McpTypes.ClaudeDesktop,
|
||||||
|
configStatus = "Not Configured"
|
||||||
|
},
|
||||||
|
new() {
|
||||||
|
name = "Cursor",
|
||||||
|
windowsConfigPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
".cursor",
|
||||||
|
"mcp.json"
|
||||||
|
),
|
||||||
|
linuxConfigPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
".cursor",
|
||||||
|
"mcp.json"
|
||||||
|
),
|
||||||
|
mcpType = McpTypes.Cursor,
|
||||||
|
configStatus = "Not Configured"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize status enums after construction
|
||||||
|
public McpClients()
|
||||||
|
{
|
||||||
|
foreach (var client in clients)
|
||||||
|
{
|
||||||
|
if (client.configStatus == "Not Configured")
|
||||||
|
{
|
||||||
|
client.status = McpStatus.NotConfigured;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 711b86bbc1f661e4fb2c822e14970e16
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace MCPServer.Editor.Helpers
|
namespace UnityMCP.Editor.Helpers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Helper class for Vector3 operations
|
/// Helper class for Vector3 operations
|
||||||
|
|
|
||||||
|
|
@ -1,645 +0,0 @@
|
||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Text;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
public class DefaultServerConfig : ServerConfig
|
|
||||||
{
|
|
||||||
public new string unityHost = "localhost";
|
|
||||||
public new int unityPort = 6400;
|
|
||||||
public new int mcpPort = 6500;
|
|
||||||
public new float connectionTimeout = 15.0f;
|
|
||||||
public new int bufferSize = 32768;
|
|
||||||
public new string logLevel = "INFO";
|
|
||||||
public new string logFormat = "%(asctime)s - %(name)s - %(levelname)s - %(message)s";
|
|
||||||
public new int maxRetries = 3;
|
|
||||||
public new float retryDelay = 1.0f;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class MCPConfig
|
|
||||||
{
|
|
||||||
[JsonProperty("mcpServers")]
|
|
||||||
public MCPConfigServers mcpServers;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class MCPConfigServers
|
|
||||||
{
|
|
||||||
[JsonProperty("unityMCP")]
|
|
||||||
public MCPConfigServer unityMCP;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class MCPConfigServer
|
|
||||||
{
|
|
||||||
[JsonProperty("command")]
|
|
||||||
public string command;
|
|
||||||
|
|
||||||
[JsonProperty("args")]
|
|
||||||
public string[] args;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class ServerConfig
|
|
||||||
{
|
|
||||||
[JsonProperty("unity_host")]
|
|
||||||
public string unityHost = "localhost";
|
|
||||||
|
|
||||||
[JsonProperty("unity_port")]
|
|
||||||
public int unityPort;
|
|
||||||
|
|
||||||
[JsonProperty("mcp_port")]
|
|
||||||
public int mcpPort;
|
|
||||||
|
|
||||||
[JsonProperty("connection_timeout")]
|
|
||||||
public float connectionTimeout;
|
|
||||||
|
|
||||||
[JsonProperty("buffer_size")]
|
|
||||||
public int bufferSize;
|
|
||||||
|
|
||||||
[JsonProperty("log_level")]
|
|
||||||
public string logLevel;
|
|
||||||
|
|
||||||
[JsonProperty("log_format")]
|
|
||||||
public string logFormat;
|
|
||||||
|
|
||||||
[JsonProperty("max_retries")]
|
|
||||||
public int maxRetries;
|
|
||||||
|
|
||||||
[JsonProperty("retry_delay")]
|
|
||||||
public float retryDelay;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MCPEditorWindow : EditorWindow
|
|
||||||
{
|
|
||||||
private bool isUnityBridgeRunning = false;
|
|
||||||
private Vector2 scrollPosition;
|
|
||||||
private string claudeConfigStatus = "Not configured";
|
|
||||||
private string pythonServerStatus = "Not Connected";
|
|
||||||
private Color pythonServerStatusColor = Color.red;
|
|
||||||
private const int unityPort = 6400; // Hardcoded Unity port
|
|
||||||
private const int mcpPort = 6500; // Hardcoded MCP port
|
|
||||||
private const float CONNECTION_CHECK_INTERVAL = 2f; // Check every 2 seconds
|
|
||||||
private float lastCheckTime = 0f;
|
|
||||||
|
|
||||||
[MenuItem("Window/Unity MCP")]
|
|
||||||
public static void ShowWindow()
|
|
||||||
{
|
|
||||||
GetWindow<MCPEditorWindow>("MCP Editor");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnEnable()
|
|
||||||
{
|
|
||||||
// Check initial states
|
|
||||||
isUnityBridgeRunning = UnityMCPBridge.IsRunning;
|
|
||||||
CheckPythonServerConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Update()
|
|
||||||
{
|
|
||||||
// Check Python server connection periodically
|
|
||||||
if (Time.realtimeSinceStartup - lastCheckTime >= CONNECTION_CHECK_INTERVAL)
|
|
||||||
{
|
|
||||||
CheckPythonServerConnection();
|
|
||||||
lastCheckTime = Time.realtimeSinceStartup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void CheckPythonServerConnection()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var client = new TcpClient())
|
|
||||||
{
|
|
||||||
// Try to connect with a short timeout
|
|
||||||
var connectTask = client.ConnectAsync("localhost", unityPort);
|
|
||||||
if (await Task.WhenAny(connectTask, Task.Delay(1000)) == connectTask)
|
|
||||||
{
|
|
||||||
// Try to send a ping message to verify connection is alive
|
|
||||||
try
|
|
||||||
{
|
|
||||||
NetworkStream stream = client.GetStream();
|
|
||||||
byte[] pingMessage = Encoding.UTF8.GetBytes("ping");
|
|
||||||
await stream.WriteAsync(pingMessage, 0, pingMessage.Length);
|
|
||||||
|
|
||||||
// Wait for response with timeout
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
var readTask = stream.ReadAsync(buffer, 0, buffer.Length);
|
|
||||||
if (await Task.WhenAny(readTask, Task.Delay(1000)) == readTask)
|
|
||||||
{
|
|
||||||
// Connection successful and responsive
|
|
||||||
pythonServerStatus = "Connected";
|
|
||||||
pythonServerStatusColor = Color.green;
|
|
||||||
UnityEngine.Debug.Log($"Python server connected successfully on port {unityPort}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// No response received
|
|
||||||
pythonServerStatus = "No Response";
|
|
||||||
pythonServerStatusColor = Color.yellow;
|
|
||||||
UnityEngine.Debug.LogWarning($"Python server not responding on port {unityPort}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
// Connection established but communication failed
|
|
||||||
pythonServerStatus = "Communication Error";
|
|
||||||
pythonServerStatusColor = Color.yellow;
|
|
||||||
UnityEngine.Debug.LogWarning($"Error communicating with Python server: {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Connection failed
|
|
||||||
pythonServerStatus = "Not Connected";
|
|
||||||
pythonServerStatusColor = Color.red;
|
|
||||||
UnityEngine.Debug.LogWarning($"Python server is not running or not accessible on port {unityPort}");
|
|
||||||
}
|
|
||||||
client.Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
pythonServerStatus = "Connection Error";
|
|
||||||
pythonServerStatusColor = Color.red;
|
|
||||||
UnityEngine.Debug.LogError($"Error checking Python server connection: {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnGUI()
|
|
||||||
{
|
|
||||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
|
||||||
|
|
||||||
EditorGUILayout.Space(10);
|
|
||||||
EditorGUILayout.LabelField("MCP Editor", EditorStyles.boldLabel);
|
|
||||||
EditorGUILayout.Space(10);
|
|
||||||
|
|
||||||
// Python Server Status Section
|
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
||||||
EditorGUILayout.LabelField("Python Server Status", EditorStyles.boldLabel);
|
|
||||||
|
|
||||||
// Status bar
|
|
||||||
var statusRect = EditorGUILayout.BeginHorizontal();
|
|
||||||
EditorGUI.DrawRect(new Rect(statusRect.x, statusRect.y, 10, 20), pythonServerStatusColor);
|
|
||||||
EditorGUILayout.LabelField(pythonServerStatus);
|
|
||||||
EditorGUILayout.EndHorizontal();
|
|
||||||
|
|
||||||
EditorGUILayout.LabelField($"Unity Port: {unityPort}");
|
|
||||||
EditorGUILayout.LabelField($"MCP Port: {mcpPort}");
|
|
||||||
EditorGUILayout.HelpBox("Start the Python server using command line: 'uv run server.py' in the Python directory", MessageType.Info);
|
|
||||||
EditorGUILayout.EndVertical();
|
|
||||||
|
|
||||||
EditorGUILayout.Space(10);
|
|
||||||
|
|
||||||
// Unity Bridge Section
|
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
||||||
EditorGUILayout.LabelField("Unity MCP Bridge", EditorStyles.boldLabel);
|
|
||||||
EditorGUILayout.LabelField($"Status: {(isUnityBridgeRunning ? "Running" : "Stopped")}");
|
|
||||||
EditorGUILayout.LabelField($"Port: {unityPort}");
|
|
||||||
|
|
||||||
if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge"))
|
|
||||||
{
|
|
||||||
ToggleUnityBridge();
|
|
||||||
}
|
|
||||||
EditorGUILayout.EndVertical();
|
|
||||||
|
|
||||||
EditorGUILayout.Space(10);
|
|
||||||
|
|
||||||
// Claude Desktop Configuration Section
|
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
||||||
EditorGUILayout.LabelField("Claude Desktop Configuration", EditorStyles.boldLabel);
|
|
||||||
EditorGUILayout.LabelField($"Status: {claudeConfigStatus}");
|
|
||||||
|
|
||||||
if (GUILayout.Button("Configure Claude Desktop"))
|
|
||||||
{
|
|
||||||
ConfigureClaudeDesktop();
|
|
||||||
}
|
|
||||||
EditorGUILayout.EndVertical();
|
|
||||||
|
|
||||||
EditorGUILayout.EndScrollView();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ToggleUnityBridge()
|
|
||||||
{
|
|
||||||
if (isUnityBridgeRunning)
|
|
||||||
{
|
|
||||||
UnityMCPBridge.Stop();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UnityMCPBridge.Start();
|
|
||||||
}
|
|
||||||
isUnityBridgeRunning = !isUnityBridgeRunning;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConfigureClaudeDesktop()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Determine the config file path based on OS
|
|
||||||
string configPath;
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
{
|
|
||||||
configPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
||||||
"Claude",
|
|
||||||
"claude_desktop_config.json"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
||||||
{
|
|
||||||
configPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
||||||
"Library",
|
|
||||||
"Application Support",
|
|
||||||
"Claude",
|
|
||||||
"claude_desktop_config.json"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
claudeConfigStatus = "Unsupported OS";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create directory if it doesn't exist
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(configPath));
|
|
||||||
|
|
||||||
// Find the server.py file location
|
|
||||||
string serverPath = null;
|
|
||||||
string pythonDir = null;
|
|
||||||
|
|
||||||
// List of possible locations to search
|
|
||||||
var possiblePaths = new List<string>
|
|
||||||
{
|
|
||||||
// Search in Assets folder - Manual installation
|
|
||||||
Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python", "server.py")),
|
|
||||||
Path.GetFullPath(Path.Combine(Application.dataPath, "Packages", "com.justinpbarnett.unity-mcp", "Python", "server.py")),
|
|
||||||
|
|
||||||
// Search in package cache - Package manager installation
|
|
||||||
Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Library", "PackageCache", "com.justinpbarnett.unity-mcp@*", "Python", "server.py")),
|
|
||||||
|
|
||||||
// Search in package manager packages - Git installation
|
|
||||||
Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Packages", "com.justinpbarnett.unity-mcp", "Python", "server.py"))
|
|
||||||
};
|
|
||||||
|
|
||||||
UnityEngine.Debug.Log("Searching for server.py in the following locations:");
|
|
||||||
|
|
||||||
// First try with explicit paths
|
|
||||||
foreach (var path in possiblePaths)
|
|
||||||
{
|
|
||||||
// Skip wildcard paths for now
|
|
||||||
if (path.Contains("*")) continue;
|
|
||||||
|
|
||||||
UnityEngine.Debug.Log($"Checking: {path}");
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
serverPath = path;
|
|
||||||
pythonDir = Path.GetDirectoryName(serverPath);
|
|
||||||
UnityEngine.Debug.Log($"Found server.py at: {serverPath}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not found, try with wildcard paths (package cache with version)
|
|
||||||
if (serverPath == null)
|
|
||||||
{
|
|
||||||
foreach (var path in possiblePaths)
|
|
||||||
{
|
|
||||||
if (!path.Contains("*")) continue;
|
|
||||||
|
|
||||||
string directoryPath = Path.GetDirectoryName(path);
|
|
||||||
string searchPattern = Path.GetFileName(Path.GetDirectoryName(path));
|
|
||||||
string parentDir = Path.GetDirectoryName(directoryPath);
|
|
||||||
|
|
||||||
if (Directory.Exists(parentDir))
|
|
||||||
{
|
|
||||||
var matchingDirs = Directory.GetDirectories(parentDir, searchPattern);
|
|
||||||
UnityEngine.Debug.Log($"Searching in: {parentDir} for pattern: {searchPattern}, found {matchingDirs.Length} matches");
|
|
||||||
|
|
||||||
foreach (var dir in matchingDirs)
|
|
||||||
{
|
|
||||||
string candidatePath = Path.Combine(dir, "Python", "server.py");
|
|
||||||
UnityEngine.Debug.Log($"Checking: {candidatePath}");
|
|
||||||
|
|
||||||
if (File.Exists(candidatePath))
|
|
||||||
{
|
|
||||||
serverPath = candidatePath;
|
|
||||||
pythonDir = Path.GetDirectoryName(serverPath);
|
|
||||||
UnityEngine.Debug.Log($"Found server.py at: {serverPath}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverPath != null) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverPath == null || !File.Exists(serverPath))
|
|
||||||
{
|
|
||||||
ShowManualConfigurationInstructions(configPath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UnityEngine.Debug.Log($"Using server.py at: {serverPath}");
|
|
||||||
UnityEngine.Debug.Log($"Python directory: {pythonDir}");
|
|
||||||
|
|
||||||
// Load existing configuration if it exists
|
|
||||||
dynamic existingConfig = null;
|
|
||||||
if (File.Exists(configPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string existingJson = File.ReadAllText(configPath);
|
|
||||||
existingConfig = JsonConvert.DeserializeObject(existingJson);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
UnityEngine.Debug.LogWarning($"Failed to parse existing Claude config: {ex.Message}. Creating new config.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no existing config or parsing failed, create a new one
|
|
||||||
if (existingConfig == null)
|
|
||||||
{
|
|
||||||
existingConfig = new
|
|
||||||
{
|
|
||||||
mcpServers = new Dictionary<string, object>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the Unity MCP server configuration
|
|
||||||
var unityMCPConfig = new MCPConfigServer
|
|
||||||
{
|
|
||||||
command = "uv",
|
|
||||||
args = new[]
|
|
||||||
{
|
|
||||||
"--directory",
|
|
||||||
pythonDir,
|
|
||||||
"run",
|
|
||||||
"server.py"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Add or update the Unity MCP configuration while preserving the rest
|
|
||||||
var mcpServers = existingConfig.mcpServers as Newtonsoft.Json.Linq.JObject
|
|
||||||
?? new Newtonsoft.Json.Linq.JObject();
|
|
||||||
|
|
||||||
mcpServers["unityMCP"] = Newtonsoft.Json.Linq.JToken.FromObject(unityMCPConfig);
|
|
||||||
existingConfig.mcpServers = mcpServers;
|
|
||||||
// Serialize and write to file with proper formatting
|
|
||||||
var jsonSettings = new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
Formatting = Formatting.Indented
|
|
||||||
};
|
|
||||||
string jsonConfig = JsonConvert.SerializeObject(existingConfig, jsonSettings);
|
|
||||||
File.WriteAllText(configPath, jsonConfig);
|
|
||||||
|
|
||||||
claudeConfigStatus = "Configured successfully";
|
|
||||||
UnityEngine.Debug.Log($"Claude Desktop configuration saved to: {configPath}");
|
|
||||||
UnityEngine.Debug.Log($"Configuration contents:\n{jsonConfig}");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
// Determine the config file path based on OS for error message
|
|
||||||
string configPath = "";
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
{
|
|
||||||
configPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
||||||
"Claude",
|
|
||||||
"claude_desktop_config.json"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
||||||
{
|
|
||||||
configPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
||||||
"Library",
|
|
||||||
"Application Support",
|
|
||||||
"Claude",
|
|
||||||
"claude_desktop_config.json"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ShowManualConfigurationInstructions(configPath);
|
|
||||||
UnityEngine.Debug.LogError($"Failed to configure Claude Desktop: {e.Message}\n{e.StackTrace}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowManualConfigurationInstructions(string configPath)
|
|
||||||
{
|
|
||||||
claudeConfigStatus = "Error: Manual configuration required";
|
|
||||||
|
|
||||||
// Get the Python directory path using Package Manager API
|
|
||||||
string pythonDir = FindPackagePythonDirectory();
|
|
||||||
|
|
||||||
// Create the manual configuration message
|
|
||||||
var jsonConfig = new MCPConfig
|
|
||||||
{
|
|
||||||
mcpServers = new MCPConfigServers
|
|
||||||
{
|
|
||||||
unityMCP = new MCPConfigServer
|
|
||||||
{
|
|
||||||
command = "uv",
|
|
||||||
args = new[]
|
|
||||||
{
|
|
||||||
"--directory",
|
|
||||||
pythonDir,
|
|
||||||
"run",
|
|
||||||
"server.py"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var jsonSettings = new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
Formatting = Formatting.Indented
|
|
||||||
};
|
|
||||||
string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings);
|
|
||||||
|
|
||||||
// Show a dedicated configuration window instead of console logs
|
|
||||||
ManualConfigWindow.ShowWindow(configPath, manualConfigJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string FindPackagePythonDirectory()
|
|
||||||
{
|
|
||||||
string pythonDir = "/path/to/your/unity-mcp/Python";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Try to find the package using Package Manager API
|
|
||||||
var request = UnityEditor.PackageManager.Client.List();
|
|
||||||
while (!request.IsCompleted) { } // Wait for the request to complete
|
|
||||||
|
|
||||||
if (request.Status == UnityEditor.PackageManager.StatusCode.Success)
|
|
||||||
{
|
|
||||||
foreach (var package in request.Result)
|
|
||||||
{
|
|
||||||
UnityEngine.Debug.Log($"Package: {package.name}, Path: {package.resolvedPath}");
|
|
||||||
|
|
||||||
if (package.name == "com.justinpbarnett.unity-mcp")
|
|
||||||
{
|
|
||||||
string packagePath = package.resolvedPath;
|
|
||||||
string potentialPythonDir = Path.Combine(packagePath, "Python");
|
|
||||||
|
|
||||||
if (Directory.Exists(potentialPythonDir) &&
|
|
||||||
File.Exists(Path.Combine(potentialPythonDir, "server.py")))
|
|
||||||
{
|
|
||||||
UnityEngine.Debug.Log($"Found package Python directory at: {potentialPythonDir}");
|
|
||||||
return potentialPythonDir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (request.Error != null)
|
|
||||||
{
|
|
||||||
UnityEngine.Debug.LogError("Failed to list packages: " + request.Error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not found via Package Manager, try manual approaches
|
|
||||||
// First check for local installation
|
|
||||||
string[] possibleDirs = {
|
|
||||||
Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python"))
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var dir in possibleDirs)
|
|
||||||
{
|
|
||||||
UnityEngine.Debug.Log($"Checking local directory: {dir}");
|
|
||||||
if (Directory.Exists(dir) && File.Exists(Path.Combine(dir, "server.py")))
|
|
||||||
{
|
|
||||||
UnityEngine.Debug.Log($"Found local Python directory at: {dir}");
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If still not found, return the placeholder path
|
|
||||||
UnityEngine.Debug.LogWarning("Could not find Python directory, using placeholder path");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
UnityEngine.Debug.LogError($"Error finding package path: {e.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return pythonDir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Editor window to display manual configuration instructions
|
|
||||||
public class ManualConfigWindow : EditorWindow
|
|
||||||
{
|
|
||||||
private string configPath;
|
|
||||||
private string configJson;
|
|
||||||
private Vector2 scrollPos;
|
|
||||||
private bool pathCopied = false;
|
|
||||||
private bool jsonCopied = false;
|
|
||||||
private float copyFeedbackTimer = 0;
|
|
||||||
|
|
||||||
public static void ShowWindow(string configPath, string configJson)
|
|
||||||
{
|
|
||||||
var window = GetWindow<ManualConfigWindow>("Manual Configuration");
|
|
||||||
window.configPath = configPath;
|
|
||||||
window.configJson = configJson;
|
|
||||||
window.minSize = new Vector2(500, 400);
|
|
||||||
window.Show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnGUI()
|
|
||||||
{
|
|
||||||
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
|
|
||||||
|
|
||||||
// Header
|
|
||||||
EditorGUILayout.Space(10);
|
|
||||||
EditorGUILayout.LabelField("Claude Desktop Manual Configuration", EditorStyles.boldLabel);
|
|
||||||
EditorGUILayout.Space(10);
|
|
||||||
|
|
||||||
// Instructions
|
|
||||||
EditorGUILayout.LabelField("The automatic configuration failed. Please follow these steps:", EditorStyles.boldLabel);
|
|
||||||
EditorGUILayout.Space(5);
|
|
||||||
|
|
||||||
EditorGUILayout.LabelField("1. Open Claude Desktop and go to Settings > Developer > Edit Config", EditorStyles.wordWrappedLabel);
|
|
||||||
EditorGUILayout.LabelField("2. Create or edit the configuration file at:", EditorStyles.wordWrappedLabel);
|
|
||||||
|
|
||||||
// Config path section with copy button
|
|
||||||
EditorGUILayout.BeginHorizontal();
|
|
||||||
EditorGUILayout.SelectableLabel(configPath, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
|
|
||||||
|
|
||||||
if (GUILayout.Button("Copy Path", GUILayout.Width(80)))
|
|
||||||
{
|
|
||||||
EditorGUIUtility.systemCopyBuffer = configPath;
|
|
||||||
pathCopied = true;
|
|
||||||
copyFeedbackTimer = 2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorGUILayout.EndHorizontal();
|
|
||||||
|
|
||||||
if (pathCopied)
|
|
||||||
{
|
|
||||||
EditorGUILayout.LabelField("Path copied to clipboard!", EditorStyles.miniLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorGUILayout.Space(10);
|
|
||||||
|
|
||||||
// JSON configuration
|
|
||||||
EditorGUILayout.LabelField("3. Paste the following JSON configuration:", EditorStyles.wordWrappedLabel);
|
|
||||||
EditorGUILayout.Space(5);
|
|
||||||
|
|
||||||
EditorGUILayout.LabelField("Make sure to replace the Python path if necessary:", EditorStyles.wordWrappedLabel);
|
|
||||||
EditorGUILayout.Space(5);
|
|
||||||
|
|
||||||
// JSON text area with copy button
|
|
||||||
GUIStyle textAreaStyle = new GUIStyle(EditorStyles.textArea)
|
|
||||||
{
|
|
||||||
wordWrap = true,
|
|
||||||
richText = true
|
|
||||||
};
|
|
||||||
|
|
||||||
EditorGUILayout.BeginHorizontal();
|
|
||||||
EditorGUILayout.SelectableLabel(configJson, textAreaStyle, GUILayout.MinHeight(200));
|
|
||||||
EditorGUILayout.EndHorizontal();
|
|
||||||
|
|
||||||
if (GUILayout.Button("Copy JSON Configuration"))
|
|
||||||
{
|
|
||||||
EditorGUIUtility.systemCopyBuffer = configJson;
|
|
||||||
jsonCopied = true;
|
|
||||||
copyFeedbackTimer = 2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonCopied)
|
|
||||||
{
|
|
||||||
EditorGUILayout.LabelField("JSON copied to clipboard!", EditorStyles.miniLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorGUILayout.Space(10);
|
|
||||||
|
|
||||||
// Additional note
|
|
||||||
EditorGUILayout.HelpBox("After configuring, restart Claude Desktop to apply the changes.", MessageType.Info);
|
|
||||||
|
|
||||||
EditorGUILayout.EndScrollView();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Update()
|
|
||||||
{
|
|
||||||
// Handle the feedback message timer
|
|
||||||
if (copyFeedbackTimer > 0)
|
|
||||||
{
|
|
||||||
copyFeedbackTimer -= Time.deltaTime;
|
|
||||||
if (copyFeedbackTimer <= 0)
|
|
||||||
{
|
|
||||||
pathCopied = false;
|
|
||||||
jsonCopied = false;
|
|
||||||
Repaint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace MCPServer.Editor.Models
|
namespace UnityMCP.Editor.Models
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a command received from the MCP client
|
/// Represents a command received from the MCP client
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace UnityMCP.Editor.Models
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class MCPConfig
|
||||||
|
{
|
||||||
|
[JsonProperty("mcpServers")]
|
||||||
|
public MCPConfigServers mcpServers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c17c09908f0c1524daa8b6957ce1f7f5
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace UnityMCP.Editor.Models
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class MCPConfigServer
|
||||||
|
{
|
||||||
|
[JsonProperty("command")]
|
||||||
|
public string command;
|
||||||
|
|
||||||
|
[JsonProperty("args")]
|
||||||
|
public string[] args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5fae9d995f514e9498e9613e2cdbeca9
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace UnityMCP.Editor.Models
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class MCPConfigServers
|
||||||
|
{
|
||||||
|
[JsonProperty("unityMCP")]
|
||||||
|
public MCPConfigServer unityMCP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bcb583553e8173b49be71a5c43bd9502
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
namespace UnityMCP.Editor.Models
|
||||||
|
{
|
||||||
|
public class McpClient
|
||||||
|
{
|
||||||
|
public string name;
|
||||||
|
public string windowsConfigPath;
|
||||||
|
public string linuxConfigPath;
|
||||||
|
public McpTypes mcpType;
|
||||||
|
public string configStatus;
|
||||||
|
public McpStatus status = McpStatus.NotConfigured;
|
||||||
|
|
||||||
|
// Helper method to convert the enum to a display string
|
||||||
|
public string GetStatusDisplayString()
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
McpStatus.NotConfigured => "Not Configured",
|
||||||
|
McpStatus.Configured => "Configured",
|
||||||
|
McpStatus.Running => "Running",
|
||||||
|
McpStatus.Connected => "Connected",
|
||||||
|
McpStatus.IncorrectPath => "Incorrect Path",
|
||||||
|
McpStatus.CommunicationError => "Communication Error",
|
||||||
|
McpStatus.NoResponse => "No Response",
|
||||||
|
McpStatus.UnsupportedOS => "Unsupported OS",
|
||||||
|
McpStatus.MissingConfig => "Missing UnityMCP Config",
|
||||||
|
McpStatus.Error => configStatus.StartsWith("Error:") ? configStatus : "Error",
|
||||||
|
_ => "Unknown"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to set both status enum and string for backward compatibility
|
||||||
|
public void SetStatus(McpStatus newStatus, string errorDetails = null)
|
||||||
|
{
|
||||||
|
status = newStatus;
|
||||||
|
|
||||||
|
if (newStatus == McpStatus.Error && !string.IsNullOrEmpty(errorDetails))
|
||||||
|
{
|
||||||
|
configStatus = $"Error: {errorDetails}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
configStatus = GetStatusDisplayString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b1afa56984aec0d41808edcebf805e6a
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
namespace UnityMCP.Editor.Models
|
||||||
|
{
|
||||||
|
// Enum representing the various status states for MCP clients
|
||||||
|
public enum McpStatus
|
||||||
|
{
|
||||||
|
NotConfigured, // Not set up yet
|
||||||
|
Configured, // Successfully configured
|
||||||
|
Running, // Service is running
|
||||||
|
Connected, // Successfully connected
|
||||||
|
IncorrectPath, // Configuration has incorrect paths
|
||||||
|
CommunicationError, // Connected but communication issues
|
||||||
|
NoResponse, // Connected but not responding
|
||||||
|
MissingConfig, // Config file exists but missing required elements
|
||||||
|
UnsupportedOS, // OS is not supported
|
||||||
|
Error // General error state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: aa63057c9e5282d4887352578bf49971
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace UnityMCP.Editor.Models
|
||||||
|
{
|
||||||
|
public enum McpTypes
|
||||||
|
{
|
||||||
|
ClaudeDesktop,
|
||||||
|
Cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace UnityMCP.Editor.Models
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class ServerConfig
|
||||||
|
{
|
||||||
|
[JsonProperty("unity_host")]
|
||||||
|
public string unityHost = "localhost";
|
||||||
|
|
||||||
|
[JsonProperty("unity_port")]
|
||||||
|
public int unityPort;
|
||||||
|
|
||||||
|
[JsonProperty("mcp_port")]
|
||||||
|
public int mcpPort;
|
||||||
|
|
||||||
|
[JsonProperty("connection_timeout")]
|
||||||
|
public float connectionTimeout;
|
||||||
|
|
||||||
|
[JsonProperty("buffer_size")]
|
||||||
|
public int bufferSize;
|
||||||
|
|
||||||
|
[JsonProperty("log_level")]
|
||||||
|
public string logLevel;
|
||||||
|
|
||||||
|
[JsonProperty("log_format")]
|
||||||
|
public string logFormat;
|
||||||
|
|
||||||
|
[JsonProperty("max_retries")]
|
||||||
|
public int maxRetries;
|
||||||
|
|
||||||
|
[JsonProperty("retry_delay")]
|
||||||
|
public float retryDelay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e4e45386fcc282249907c2e3c7e5d9c6
|
||||||
|
|
@ -3,30 +3,28 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using MCPServer.Editor.Models;
|
|
||||||
using MCPServer.Editor.Commands;
|
|
||||||
using MCPServer.Editor.Helpers;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using UnityMCP.Editor.Models;
|
||||||
|
using UnityMCP.Editor.Commands;
|
||||||
|
|
||||||
[InitializeOnLoad]
|
namespace UnityMCP.Editor
|
||||||
public static partial class UnityMCPBridge
|
|
||||||
{
|
{
|
||||||
|
[InitializeOnLoad]
|
||||||
|
public static partial class UnityMCPBridge
|
||||||
|
{
|
||||||
private static TcpListener listener;
|
private static TcpListener listener;
|
||||||
private static bool isRunning = false;
|
private static bool isRunning = false;
|
||||||
private static readonly object lockObj = new object();
|
private static readonly object lockObj = new();
|
||||||
private static Dictionary<string, (string commandJson, TaskCompletionSource<string> tcs)> commandQueue = new();
|
private static Dictionary<string, (string commandJson, TaskCompletionSource<string> tcs)> commandQueue = new();
|
||||||
private static readonly int unityPort = 6400; // Hardcoded port
|
private static readonly int unityPort = 6400; // Hardcoded port
|
||||||
|
|
||||||
// Add public property to expose running state
|
|
||||||
public static bool IsRunning => isRunning;
|
public static bool IsRunning => isRunning;
|
||||||
|
|
||||||
// Add method to check existence of a folder
|
|
||||||
public static bool FolderExists(string path)
|
public static bool FolderExists(string path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
|
|
@ -343,4 +341,5 @@ public static partial class UnityMCPBridge
|
||||||
return "Could not summarize parameters";
|
return "Could not summarize parameters";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d2ee39f5d4171184eb208e865c1ef4c1
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using UnityMCP.Editor.Models;
|
||||||
|
|
||||||
|
namespace UnityMCP.Editor.Windows
|
||||||
|
{
|
||||||
|
// Editor window to display manual configuration instructions
|
||||||
|
public class ManualConfigEditorWindow : EditorWindow
|
||||||
|
{
|
||||||
|
private string configPath;
|
||||||
|
private string configJson;
|
||||||
|
private Vector2 scrollPos;
|
||||||
|
private bool pathCopied = false;
|
||||||
|
private bool jsonCopied = false;
|
||||||
|
private float copyFeedbackTimer = 0;
|
||||||
|
private McpClient mcpClient;
|
||||||
|
|
||||||
|
public static void ShowWindow(string configPath, string configJson, McpClient mcpClient)
|
||||||
|
{
|
||||||
|
var window = GetWindow<ManualConfigEditorWindow>("Manual Configuration");
|
||||||
|
window.configPath = configPath;
|
||||||
|
window.configJson = configJson;
|
||||||
|
window.mcpClient = mcpClient;
|
||||||
|
window.minSize = new Vector2(500, 400);
|
||||||
|
window.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGUI()
|
||||||
|
{
|
||||||
|
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
|
||||||
|
|
||||||
|
// Header with improved styling
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
|
||||||
|
EditorGUI.DrawRect(new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height), new Color(0.2f, 0.2f, 0.2f, 0.1f));
|
||||||
|
GUI.Label(new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
|
||||||
|
mcpClient.name + " Manual Configuration", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
// Instructions with improved styling
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
|
||||||
|
Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
|
||||||
|
EditorGUI.DrawRect(new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height), new Color(0.1f, 0.1f, 0.1f, 0.2f));
|
||||||
|
GUI.Label(new Rect(headerRect.x + 8, headerRect.y + 4, headerRect.width - 16, headerRect.height),
|
||||||
|
"The automatic configuration failed. Please follow these steps:", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel)
|
||||||
|
{
|
||||||
|
margin = new RectOffset(10, 10, 5, 5)
|
||||||
|
};
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField("1. Open " + mcpClient.name + " config file by either:", instructionStyle);
|
||||||
|
if (mcpClient.mcpType == McpTypes.ClaudeDesktop)
|
||||||
|
{
|
||||||
|
EditorGUILayout.LabelField(" a) Going to Settings > Developer > Edit Config", instructionStyle);
|
||||||
|
}
|
||||||
|
else if (mcpClient.mcpType == McpTypes.Cursor)
|
||||||
|
{
|
||||||
|
EditorGUILayout.LabelField(" a) Going to File > Preferences > Cursor Settings > MCP > Add new global MCP server", instructionStyle);
|
||||||
|
}
|
||||||
|
EditorGUILayout.LabelField(" OR", instructionStyle);
|
||||||
|
EditorGUILayout.LabelField(" b) Opening the configuration file at:", instructionStyle);
|
||||||
|
|
||||||
|
// Path section with improved styling
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
string displayPath;
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
displayPath = mcpClient.windowsConfigPath;
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
{
|
||||||
|
displayPath = mcpClient.linuxConfigPath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
displayPath = configPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent text overflow by allowing the text field to wrap
|
||||||
|
GUIStyle pathStyle = new(EditorStyles.textField)
|
||||||
|
{
|
||||||
|
wordWrap = true
|
||||||
|
};
|
||||||
|
|
||||||
|
EditorGUILayout.TextField(displayPath, pathStyle, GUILayout.Height(EditorGUIUtility.singleLineHeight));
|
||||||
|
|
||||||
|
// Copy button with improved styling
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
GUIStyle copyButtonStyle = new(GUI.skin.button)
|
||||||
|
{
|
||||||
|
padding = new RectOffset(15, 15, 5, 5),
|
||||||
|
margin = new RectOffset(10, 10, 5, 5)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (GUILayout.Button("Copy Path", copyButtonStyle, GUILayout.Height(25), GUILayout.Width(100)))
|
||||||
|
{
|
||||||
|
EditorGUIUtility.systemCopyBuffer = displayPath;
|
||||||
|
pathCopied = true;
|
||||||
|
copyFeedbackTimer = 2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GUILayout.Button("Open File", copyButtonStyle, GUILayout.Height(25), GUILayout.Width(100)))
|
||||||
|
{
|
||||||
|
// Open the file using the system's default application
|
||||||
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = displayPath,
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathCopied)
|
||||||
|
{
|
||||||
|
GUIStyle feedbackStyle = new(EditorStyles.label);
|
||||||
|
feedbackStyle.normal.textColor = Color.green;
|
||||||
|
EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField("2. Paste the following JSON configuration:", instructionStyle);
|
||||||
|
|
||||||
|
// JSON section with improved styling
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
|
||||||
|
// Improved text area for JSON with syntax highlighting colors
|
||||||
|
GUIStyle jsonStyle = new(EditorStyles.textArea)
|
||||||
|
{
|
||||||
|
font = EditorStyles.boldFont,
|
||||||
|
wordWrap = true
|
||||||
|
};
|
||||||
|
jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue
|
||||||
|
|
||||||
|
// Draw the JSON in a text area with a taller height for better readability
|
||||||
|
EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200));
|
||||||
|
|
||||||
|
// Copy JSON button with improved styling
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
|
||||||
|
if (GUILayout.Button("Copy JSON", copyButtonStyle, GUILayout.Height(25), GUILayout.Width(100)))
|
||||||
|
{
|
||||||
|
EditorGUIUtility.systemCopyBuffer = configJson;
|
||||||
|
jsonCopied = true;
|
||||||
|
copyFeedbackTimer = 2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonCopied)
|
||||||
|
{
|
||||||
|
GUIStyle feedbackStyle = new(EditorStyles.label);
|
||||||
|
feedbackStyle.normal.textColor = Color.green;
|
||||||
|
EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
EditorGUILayout.LabelField("3. Save the file and restart " + mcpClient.name, instructionStyle);
|
||||||
|
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
// Close button at the bottom
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100)))
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
EditorGUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
// Handle the feedback message timer
|
||||||
|
if (copyFeedbackTimer > 0)
|
||||||
|
{
|
||||||
|
copyFeedbackTimer -= Time.deltaTime;
|
||||||
|
if (copyFeedbackTimer <= 0)
|
||||||
|
{
|
||||||
|
pathCopied = false;
|
||||||
|
jsonCopied = false;
|
||||||
|
Repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 36798bd7b867b8e43ac86885e94f928f
|
||||||
|
|
@ -0,0 +1,663 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Text;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityMCP.Editor.Models;
|
||||||
|
using UnityMCP.Editor.Data;
|
||||||
|
|
||||||
|
namespace UnityMCP.Editor.Windows
|
||||||
|
{
|
||||||
|
public class UnityMCPEditorWindow : EditorWindow
|
||||||
|
{
|
||||||
|
private bool isUnityBridgeRunning = false;
|
||||||
|
private Vector2 scrollPosition;
|
||||||
|
private string claudeConfigStatus = "Not configured";
|
||||||
|
private string cursorConfigStatus = "Not configured";
|
||||||
|
private string pythonServerStatus = "Not Connected";
|
||||||
|
private Color pythonServerStatusColor = Color.red;
|
||||||
|
private const int unityPort = 6400; // Hardcoded Unity port
|
||||||
|
private const int mcpPort = 6500; // Hardcoded MCP port
|
||||||
|
private const float CONNECTION_CHECK_INTERVAL = 2f; // Check every 2 seconds
|
||||||
|
private float lastCheckTime = 0f;
|
||||||
|
private McpClients mcpClients = new();
|
||||||
|
|
||||||
|
private List<string> possiblePaths = new()
|
||||||
|
{
|
||||||
|
Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python", "server.py")),
|
||||||
|
Path.GetFullPath(Path.Combine(Application.dataPath, "Packages", "com.justinpbarnett.unity-mcp", "Python", "server.py")),
|
||||||
|
Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Library", "PackageCache", "com.justinpbarnett.unity-mcp@*", "Python", "server.py")),
|
||||||
|
Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Packages", "com.justinpbarnett.unity-mcp", "Python", "server.py"))
|
||||||
|
};
|
||||||
|
|
||||||
|
[MenuItem("Window/Unity MCP")]
|
||||||
|
public static void ShowWindow()
|
||||||
|
{
|
||||||
|
GetWindow<UnityMCPEditorWindow>("MCP Editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
// Check initial states
|
||||||
|
isUnityBridgeRunning = UnityMCPBridge.IsRunning;
|
||||||
|
CheckPythonServerConnection();
|
||||||
|
foreach (McpClient mcpClient in mcpClients.clients)
|
||||||
|
{
|
||||||
|
CheckMcpConfiguration(mcpClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
// Check Python server connection periodically
|
||||||
|
if (Time.realtimeSinceStartup - lastCheckTime >= CONNECTION_CHECK_INTERVAL)
|
||||||
|
{
|
||||||
|
CheckPythonServerConnection();
|
||||||
|
lastCheckTime = Time.realtimeSinceStartup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void CheckPythonServerConnection()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var client = new TcpClient())
|
||||||
|
{
|
||||||
|
// Try to connect with a short timeout
|
||||||
|
var connectTask = client.ConnectAsync("localhost", unityPort);
|
||||||
|
if (await Task.WhenAny(connectTask, Task.Delay(1000)) == connectTask)
|
||||||
|
{
|
||||||
|
// Try to send a ping message to verify connection is alive
|
||||||
|
try
|
||||||
|
{
|
||||||
|
NetworkStream stream = client.GetStream();
|
||||||
|
byte[] pingMessage = Encoding.UTF8.GetBytes("ping");
|
||||||
|
await stream.WriteAsync(pingMessage, 0, pingMessage.Length);
|
||||||
|
|
||||||
|
// Wait for response with timeout
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
var readTask = stream.ReadAsync(buffer, 0, buffer.Length);
|
||||||
|
if (await Task.WhenAny(readTask, Task.Delay(1000)) == readTask)
|
||||||
|
{
|
||||||
|
int bytesRead = await readTask;
|
||||||
|
if (bytesRead <= 0)
|
||||||
|
{
|
||||||
|
// Received empty response
|
||||||
|
pythonServerStatus = "Invalid Response";
|
||||||
|
pythonServerStatusColor = GetStatusColor(McpStatus.NoResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the response is actually from our server
|
||||||
|
string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
||||||
|
if (response.Contains("pong"))
|
||||||
|
{
|
||||||
|
// Connection successful and responsive with valid response
|
||||||
|
pythonServerStatus = "Connected";
|
||||||
|
pythonServerStatusColor = GetStatusColor(McpStatus.Connected);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Received response but not the expected one
|
||||||
|
pythonServerStatus = "Invalid Server";
|
||||||
|
pythonServerStatusColor = GetStatusColor(McpStatus.CommunicationError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No response received
|
||||||
|
pythonServerStatus = "No Response";
|
||||||
|
pythonServerStatusColor = GetStatusColor(McpStatus.NoResponse);
|
||||||
|
UnityEngine.Debug.LogWarning($"Python server not responding on port {unityPort}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// Connection established but communication failed
|
||||||
|
pythonServerStatus = "Communication Error";
|
||||||
|
pythonServerStatusColor = GetStatusColor(McpStatus.CommunicationError);
|
||||||
|
UnityEngine.Debug.LogWarning($"Error communicating with Python server: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Connection failed
|
||||||
|
pythonServerStatus = "Not Connected";
|
||||||
|
pythonServerStatusColor = GetStatusColor(McpStatus.NotConfigured);
|
||||||
|
UnityEngine.Debug.LogWarning($"Python server is not running or not accessible on port {unityPort}");
|
||||||
|
}
|
||||||
|
client.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
pythonServerStatus = "Connection Error";
|
||||||
|
pythonServerStatusColor = GetStatusColor(McpStatus.Error);
|
||||||
|
UnityEngine.Debug.LogError($"Error checking Python server connection: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color GetStatusColor(McpStatus status)
|
||||||
|
{
|
||||||
|
// Return appropriate color based on the status enum
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
McpStatus.Configured => Color.green,
|
||||||
|
McpStatus.Running => Color.green,
|
||||||
|
McpStatus.Connected => Color.green,
|
||||||
|
McpStatus.IncorrectPath => Color.yellow,
|
||||||
|
McpStatus.CommunicationError => Color.yellow,
|
||||||
|
McpStatus.NoResponse => Color.yellow,
|
||||||
|
_ => Color.red // Default to red for error states or not configured
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigurationSection(McpClient mcpClient)
|
||||||
|
{
|
||||||
|
// Calculate if we should use half-width layout
|
||||||
|
// Minimum width for half-width layout is 400 pixels
|
||||||
|
bool useHalfWidth = position.width >= 800;
|
||||||
|
float sectionWidth = useHalfWidth ? position.width / 2 - 15 : position.width - 20;
|
||||||
|
|
||||||
|
// Begin horizontal layout if using half-width
|
||||||
|
if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) % 2 == 0)
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin section with fixed width
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox, GUILayout.Width(sectionWidth));
|
||||||
|
|
||||||
|
// Header with improved styling
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
|
||||||
|
GUI.Label(new Rect(headerRect.x + 8, headerRect.y + 4, headerRect.width - 16, headerRect.height),
|
||||||
|
mcpClient.name + " Configuration", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
// Status indicator with colored dot
|
||||||
|
Rect statusRect = EditorGUILayout.BeginHorizontal(GUILayout.Height(20));
|
||||||
|
Color statusColor = GetStatusColor(mcpClient.status);
|
||||||
|
|
||||||
|
// Draw status dot
|
||||||
|
DrawStatusDot(statusRect, statusColor);
|
||||||
|
|
||||||
|
// Status text with some padding
|
||||||
|
EditorGUILayout.LabelField(new GUIContent(" " + mcpClient.configStatus), GUILayout.Height(20), GUILayout.MinWidth(100));
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
EditorGUILayout.Space(8);
|
||||||
|
|
||||||
|
// Configure button with improved styling
|
||||||
|
GUIStyle buttonStyle = new(GUI.skin.button);
|
||||||
|
buttonStyle.padding = new RectOffset(15, 15, 5, 5);
|
||||||
|
buttonStyle.margin = new RectOffset(10, 10, 5, 5);
|
||||||
|
|
||||||
|
// Create muted button style for Manual Setup
|
||||||
|
GUIStyle mutedButtonStyle = new(buttonStyle);
|
||||||
|
|
||||||
|
if (GUILayout.Button($"Auto Configure {mcpClient.name}", buttonStyle, GUILayout.Height(28)))
|
||||||
|
{
|
||||||
|
ConfigureMcpClient(mcpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28)))
|
||||||
|
{
|
||||||
|
// Get the appropriate config path based on OS
|
||||||
|
string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||||
|
? mcpClient.windowsConfigPath
|
||||||
|
: mcpClient.linuxConfigPath;
|
||||||
|
ShowManualInstructionsWindow(configPath, mcpClient);
|
||||||
|
}
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
// End horizontal layout if using half-width and at the end of a row
|
||||||
|
if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) % 2 == 1)
|
||||||
|
{
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
}
|
||||||
|
// Add space and end the horizontal layout if last item is odd
|
||||||
|
else if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) == mcpClients.clients.Count - 1)
|
||||||
|
{
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawStatusDot(Rect statusRect, Color statusColor)
|
||||||
|
{
|
||||||
|
Rect dotRect = new(statusRect.x + 6, statusRect.y + 4, 12, 12);
|
||||||
|
Vector3 center = new(dotRect.x + dotRect.width / 2, dotRect.y + dotRect.height / 2, 0);
|
||||||
|
float radius = dotRect.width / 2;
|
||||||
|
|
||||||
|
// Draw the main dot
|
||||||
|
Handles.color = statusColor;
|
||||||
|
Handles.DrawSolidDisc(center, Vector3.forward, radius);
|
||||||
|
|
||||||
|
// Draw the border
|
||||||
|
Color borderColor = new(statusColor.r * 0.7f, statusColor.g * 0.7f, statusColor.b * 0.7f);
|
||||||
|
Handles.color = borderColor;
|
||||||
|
Handles.DrawWireDisc(center, Vector3.forward, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGUI()
|
||||||
|
{
|
||||||
|
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||||||
|
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
// Title with improved styling
|
||||||
|
Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
|
||||||
|
EditorGUI.DrawRect(new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height), new Color(0.2f, 0.2f, 0.2f, 0.1f));
|
||||||
|
GUI.Label(new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
|
||||||
|
"MCP Editor", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
// Python Server Status Section
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
EditorGUILayout.LabelField("Python Server Status", EditorStyles.boldLabel);
|
||||||
|
|
||||||
|
// Status indicator with colored dot
|
||||||
|
var statusRect = EditorGUILayout.BeginHorizontal(GUILayout.Height(20));
|
||||||
|
DrawStatusDot(statusRect, pythonServerStatusColor);
|
||||||
|
EditorGUILayout.LabelField(" " + pythonServerStatus);
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField($"Unity Port: {unityPort}");
|
||||||
|
EditorGUILayout.LabelField($"MCP Port: {mcpPort}");
|
||||||
|
EditorGUILayout.HelpBox("Your MCP client (e.g. Cursor or Claude Desktop) will start the server automatically when you start it.", MessageType.Info);
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
// Unity Bridge Section
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
EditorGUILayout.LabelField("Unity MCP Bridge", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.LabelField($"Status: {(isUnityBridgeRunning ? "Running" : "Stopped")}");
|
||||||
|
EditorGUILayout.LabelField($"Port: {unityPort}");
|
||||||
|
|
||||||
|
if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge"))
|
||||||
|
{
|
||||||
|
ToggleUnityBridge();
|
||||||
|
}
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
foreach (McpClient mcpClient in mcpClients.clients)
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
ConfigurationSection(mcpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleUnityBridge()
|
||||||
|
{
|
||||||
|
if (isUnityBridgeRunning)
|
||||||
|
{
|
||||||
|
UnityMCPBridge.Stop();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UnityMCPBridge.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
isUnityBridgeRunning = !isUnityBridgeRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private string GetPythonDirectory(List<string> possiblePaths)
|
||||||
|
{
|
||||||
|
foreach (var path in possiblePaths)
|
||||||
|
{
|
||||||
|
// Skip wildcard paths for now
|
||||||
|
if (path.Contains("*")) continue;
|
||||||
|
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
return Path.GetDirectoryName(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var path in possiblePaths)
|
||||||
|
{
|
||||||
|
if (!path.Contains("*")) continue;
|
||||||
|
|
||||||
|
string directoryPath = Path.GetDirectoryName(path);
|
||||||
|
string searchPattern = Path.GetFileName(Path.GetDirectoryName(path));
|
||||||
|
string parentDir = Path.GetDirectoryName(directoryPath);
|
||||||
|
|
||||||
|
if (Directory.Exists(parentDir))
|
||||||
|
{
|
||||||
|
var matchingDirs = Directory.GetDirectories(parentDir, searchPattern);
|
||||||
|
|
||||||
|
foreach (var dir in matchingDirs)
|
||||||
|
{
|
||||||
|
string candidatePath = Path.Combine(dir, "Python", "server.py");
|
||||||
|
|
||||||
|
if (File.Exists(candidatePath))
|
||||||
|
{
|
||||||
|
return Path.GetDirectoryName(candidatePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string WriteToConfig(string pythonDir, string configPath)
|
||||||
|
{
|
||||||
|
// Create configuration object for unityMCP
|
||||||
|
var unityMCPConfig = new MCPConfigServer
|
||||||
|
{
|
||||||
|
command = "uv",
|
||||||
|
args = new[]
|
||||||
|
{
|
||||||
|
"--directory",
|
||||||
|
pythonDir,
|
||||||
|
"run",
|
||||||
|
"server.py"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var jsonSettings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read existing config if it exists
|
||||||
|
string existingJson = "{}";
|
||||||
|
if (File.Exists(configPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
existingJson = File.ReadAllText(configPath);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogWarning($"Error reading existing config: {e.Message}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the existing JSON while preserving all properties
|
||||||
|
dynamic existingConfig = JsonConvert.DeserializeObject(existingJson);
|
||||||
|
if (existingConfig == null)
|
||||||
|
{
|
||||||
|
existingConfig = new Newtonsoft.Json.Linq.JObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure mcpServers object exists
|
||||||
|
if (existingConfig.mcpServers == null)
|
||||||
|
{
|
||||||
|
existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/update unityMCP while preserving other servers
|
||||||
|
existingConfig.mcpServers.unityMCP = JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
|
||||||
|
JsonConvert.SerializeObject(unityMCPConfig)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the merged configuration back to file
|
||||||
|
string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings);
|
||||||
|
File.WriteAllText(configPath, mergedJson);
|
||||||
|
|
||||||
|
return "Configured successfully";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowManualConfigurationInstructions(string configPath, McpClient mcpClient)
|
||||||
|
{
|
||||||
|
mcpClient.SetStatus(McpStatus.Error, "Manual configuration required");
|
||||||
|
|
||||||
|
ShowManualInstructionsWindow(configPath, mcpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method to show manual instructions without changing status
|
||||||
|
private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient)
|
||||||
|
{
|
||||||
|
// Get the Python directory path using Package Manager API
|
||||||
|
string pythonDir = FindPackagePythonDirectory();
|
||||||
|
|
||||||
|
// Create the manual configuration message
|
||||||
|
var jsonConfig = new MCPConfig
|
||||||
|
{
|
||||||
|
mcpServers = new MCPConfigServers
|
||||||
|
{
|
||||||
|
unityMCP = new MCPConfigServer
|
||||||
|
{
|
||||||
|
command = "uv",
|
||||||
|
args = new[]
|
||||||
|
{
|
||||||
|
"--directory",
|
||||||
|
pythonDir,
|
||||||
|
"run",
|
||||||
|
"server.py"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var jsonSettings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented
|
||||||
|
};
|
||||||
|
string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings);
|
||||||
|
|
||||||
|
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FindPackagePythonDirectory()
|
||||||
|
{
|
||||||
|
string pythonDir = "/path/to/your/unity-mcp/Python";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Try to find the package using Package Manager API
|
||||||
|
var request = UnityEditor.PackageManager.Client.List();
|
||||||
|
while (!request.IsCompleted) { } // Wait for the request to complete
|
||||||
|
|
||||||
|
if (request.Status == UnityEditor.PackageManager.StatusCode.Success)
|
||||||
|
{
|
||||||
|
foreach (var package in request.Result)
|
||||||
|
{
|
||||||
|
if (package.name == "com.justinpbarnett.unity-mcp")
|
||||||
|
{
|
||||||
|
string packagePath = package.resolvedPath;
|
||||||
|
string potentialPythonDir = Path.Combine(packagePath, "Python");
|
||||||
|
|
||||||
|
if (Directory.Exists(potentialPythonDir) &&
|
||||||
|
File.Exists(Path.Combine(potentialPythonDir, "server.py")))
|
||||||
|
{
|
||||||
|
return potentialPythonDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (request.Error != null)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogError("Failed to list packages: " + request.Error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found via Package Manager, try manual approaches
|
||||||
|
// First check for local installation
|
||||||
|
string[] possibleDirs = {
|
||||||
|
Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python"))
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var dir in possibleDirs)
|
||||||
|
{
|
||||||
|
if (Directory.Exists(dir) && File.Exists(Path.Combine(dir, "server.py")))
|
||||||
|
{
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still not found, return the placeholder path
|
||||||
|
UnityEngine.Debug.LogWarning("Could not find Python directory, using placeholder path");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogError($"Error finding package path: {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pythonDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ConfigureMcpClient(McpClient mcpClient)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Determine the config file path based on OS
|
||||||
|
string configPath;
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
configPath = mcpClient.windowsConfigPath;
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
{
|
||||||
|
configPath = mcpClient.linuxConfigPath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "Unsupported OS";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(configPath));
|
||||||
|
|
||||||
|
// Find the server.py file location
|
||||||
|
string pythonDir = GetPythonDirectory(possiblePaths);
|
||||||
|
|
||||||
|
if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py")))
|
||||||
|
{
|
||||||
|
ShowManualInstructionsWindow(configPath, mcpClient);
|
||||||
|
return "Manual Configuration Required";
|
||||||
|
}
|
||||||
|
|
||||||
|
string result = WriteToConfig(pythonDir, configPath);
|
||||||
|
|
||||||
|
// Update the client status after successful configuration
|
||||||
|
if (result == "Configured successfully")
|
||||||
|
{
|
||||||
|
mcpClient.SetStatus(McpStatus.Configured);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// Determine the config file path based on OS for error message
|
||||||
|
string configPath = "";
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
configPath = mcpClient.windowsConfigPath;
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
{
|
||||||
|
configPath = mcpClient.linuxConfigPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowManualInstructionsWindow(configPath, mcpClient);
|
||||||
|
UnityEngine.Debug.LogError($"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}");
|
||||||
|
return $"Failed to configure {mcpClient.name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void ShowCursorManualConfigurationInstructions(string configPath, McpClient mcpClient)
|
||||||
|
{
|
||||||
|
mcpClient.SetStatus(McpStatus.Error, "Manual configuration required");
|
||||||
|
|
||||||
|
// Get the Python directory path using Package Manager API
|
||||||
|
string pythonDir = FindPackagePythonDirectory();
|
||||||
|
|
||||||
|
// Create the manual configuration message
|
||||||
|
var jsonConfig = new MCPConfig
|
||||||
|
{
|
||||||
|
mcpServers = new MCPConfigServers
|
||||||
|
{
|
||||||
|
unityMCP = new MCPConfigServer
|
||||||
|
{
|
||||||
|
command = "uv",
|
||||||
|
args = new[]
|
||||||
|
{
|
||||||
|
"--directory",
|
||||||
|
pythonDir,
|
||||||
|
"run",
|
||||||
|
"server.py"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var jsonSettings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented
|
||||||
|
};
|
||||||
|
string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings);
|
||||||
|
|
||||||
|
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckMcpConfiguration(McpClient mcpClient)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string configPath;
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
configPath = mcpClient.windowsConfigPath;
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
{
|
||||||
|
configPath = mcpClient.linuxConfigPath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mcpClient.SetStatus(McpStatus.UnsupportedOS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(configPath))
|
||||||
|
{
|
||||||
|
mcpClient.SetStatus(McpStatus.NotConfigured);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string configJson = File.ReadAllText(configPath);
|
||||||
|
var config = JsonConvert.DeserializeObject<MCPConfig>(configJson);
|
||||||
|
|
||||||
|
if (config?.mcpServers?.unityMCP != null)
|
||||||
|
{
|
||||||
|
string pythonDir = GetPythonDirectory(possiblePaths);
|
||||||
|
if (pythonDir != null && Array.Exists(config.mcpServers.unityMCP.args, arg => arg.Contains(pythonDir, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
mcpClient.SetStatus(McpStatus.Configured);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mcpClient.SetStatus(McpStatus.IncorrectPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mcpClient.SetStatus(McpStatus.MissingConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
mcpClient.SetStatus(McpStatus.Error, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from mcp.server.fastmcp import FastMCP, Context
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
from unity_connection import get_unity_connection
|
from unity_connection import get_unity_connection
|
||||||
|
|
||||||
def register_material_tools(mcp: FastMCP):
|
def register_material_tools(mcp: FastMCP):
|
||||||
|
|
@ -9,18 +9,22 @@ def register_material_tools(mcp: FastMCP):
|
||||||
def set_material(
|
def set_material(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
object_name: str,
|
object_name: str,
|
||||||
material_name: str = None,
|
material_name: Optional[str] = None,
|
||||||
color: List[float] = None,
|
color: Optional[List[float]] = None,
|
||||||
create_if_missing: bool = True
|
create_if_missing: bool = True
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Apply or create a material for a game object.
|
Apply or create a material for a game object. If material_name is provided,
|
||||||
|
the material will be saved as a shared asset in the Materials folder.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
object_name: Target game object.
|
object_name: Target game object.
|
||||||
material_name: Optional material name.
|
material_name: Optional material name. If provided, creates/uses a shared material asset.
|
||||||
color: Optional [R, G, B] values (0.0-1.0).
|
color: Optional [R, G, B] or [R, G, B, A] values (0.0-1.0).
|
||||||
create_if_missing: Whether to create the material if it doesn't exist (default: True).
|
create_if_missing: Whether to create the material if it doesn't exist (default: True).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Status message indicating success or failure.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
unity = get_unity_connection()
|
unity = get_unity_connection()
|
||||||
|
|
@ -63,14 +67,23 @@ def register_material_tools(mcp: FastMCP):
|
||||||
return f"Error: Color {channel} value must be in the range 0.0-1.0, but got {value}."
|
return f"Error: Color {channel} value must be in the range 0.0-1.0, but got {value}."
|
||||||
|
|
||||||
# Set up parameters for the command
|
# Set up parameters for the command
|
||||||
params = {"object_name": object_name}
|
params = {
|
||||||
|
"object_name": object_name,
|
||||||
|
"create_if_missing": create_if_missing
|
||||||
|
}
|
||||||
if material_name:
|
if material_name:
|
||||||
params["material_name"] = material_name
|
params["material_name"] = material_name
|
||||||
params["create_if_missing"] = create_if_missing
|
|
||||||
if color:
|
if color:
|
||||||
params["color"] = color
|
params["color"] = color
|
||||||
|
|
||||||
result = unity.send_command("SET_MATERIAL", params)
|
result = unity.send_command("SET_MATERIAL", params)
|
||||||
return f"Applied material to {object_name}: {result.get('material_name', 'unknown')}"
|
material_name = result.get("material_name", "unknown")
|
||||||
|
material_path = result.get("path")
|
||||||
|
|
||||||
|
if material_path:
|
||||||
|
return f"Applied shared material '{material_name}' to {object_name} (saved at {material_path})"
|
||||||
|
else:
|
||||||
|
return f"Applied instance material '{material_name}' to {object_name}"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error setting material: {str(e)}"
|
return f"Error setting material: {str(e)}"
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 714de9c710feb1a42878a16b7a4e7a6f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
122
README.md
122
README.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
A Unity package that enables seamless communication between Unity and Large Language Models (LLMs) like Claude Desktop via the **Model Context Protocol (MCP)**. This server acts as a bridge, allowing Unity to send commands to and receive responses from MCP-compliant tools, empowering developers to automate workflows, manipulate assets, and control the Unity Editor programmatically.
|
A Unity package that enables seamless communication between Unity and Large Language Models (LLMs) like Claude Desktop via the **Model Context Protocol (MCP)**. This server acts as a bridge, allowing Unity to send commands to and receive responses from MCP-compliant tools, empowering developers to automate workflows, manipulate assets, and control the Unity Editor programmatically.
|
||||||
|
|
||||||
Welcome to the initial release of this open-source project! Whether you're looking to integrate LLMs into your Unity workflow or contribute to an exciting new tool, we're thrilled to have you here.
|
Welcome to the initial release of this open-source project! Whether you're looking to integrate LLMs into your Unity workflow or contribute to an exciting new tool, I appreciate you taking the time to check out my project.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ This project is perfect for developers who want to leverage LLMs to enhance thei
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Unity 2020.3 LTS or newer
|
- Unity 2020.3 LTS or newer (⚠️ only works in URP projects currently)
|
||||||
- Python 3.7 or newer
|
- Python 3.7 or newer
|
||||||
- uv package manager
|
- uv package manager
|
||||||
|
|
||||||
|
|
@ -70,16 +70,17 @@ Otherwise, installation instructions are on their website: [Install uv](https://
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
### Claude Desktop Integration
|
### MCP Client Integration
|
||||||
|
|
||||||
1. Open the Unity MCP window (`Window > Unity MCP`)
|
1. Open the Unity MCP window (`Window > Unity MCP`)
|
||||||
2. Click the "Configure Claude" button
|
2. Click the "Auto Configure" button for your desired MCP client
|
||||||
3. Follow the on-screen instructions to set up the integration
|
3. Status indicator should show green and a "Configured" message
|
||||||
|
|
||||||
Alternatively, manually configure Claude Desktop:
|
Alternatively, manually configure your MCP client:
|
||||||
|
|
||||||
1. Go to Claude > Settings > Developer > Edit Config
|
1. Open the Unity MCP window (`Window > Unity MCP`)
|
||||||
2. Edit `claude_desktop_config.json` to include:
|
2. Click the "Manually Configure" button for your desired MCP client
|
||||||
|
3. Copy the JSON code below to the config file
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -99,100 +100,15 @@ Alternatively, manually configure Claude Desktop:
|
||||||
|
|
||||||
Replace `/path/to/your/unity-mcp/Python` with the actual path to the Unity MCP Python directory.
|
Replace `/path/to/your/unity-mcp/Python` with the actual path to the Unity MCP Python directory.
|
||||||
|
|
||||||
### Cursor Integration
|
|
||||||
|
|
||||||
1. Open the Unity MCP window (`Window > Unity MCP`)
|
|
||||||
2. Click the "Configure Cursor" button
|
|
||||||
3. Follow the on-screen instructions to set up the integration
|
|
||||||
|
|
||||||
Alternatively, go to Cursor Settings > MCP and paste this as a command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv --directory "/path/to/your/unity-mcp/Python" run server.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Replace `/path/to/your/unity-mcp/Python` with the actual path to the Unity MCP Python directory.
|
|
||||||
|
|
||||||
**⚠️ Only run one instance of the MCP server (either on Cursor or Claude Desktop), not both**
|
**⚠️ Only run one instance of the MCP server (either on Cursor or Claude Desktop), not both**
|
||||||
|
|
||||||
4. **Start Claude Desktop or Cursor**
|
4. **Start Claude Desktop or Cursor**
|
||||||
- Launch your preferred tool
|
- Launch your preferred tool
|
||||||
- The Unity MCP Server will automatically connect
|
- The Unity MCP Server will automatically start and connect
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
To connect the MCP Server to tools like Claude Desktop or Cursor:
|
|
||||||
|
|
||||||
1. **Open the Unity MCP Window**
|
|
||||||
In Unity, go to `Window > Unity MCP` to open the editor window.
|
|
||||||
|
|
||||||
2. **Configure Your Tools**
|
|
||||||
|
|
||||||
- In the Unity MCP window, you'll see buttons to configure **Claude Desktop** or **Cursor**.
|
|
||||||
- Click the appropriate button and follow the on-screen instructions to set up the integration.
|
|
||||||
|
|
||||||
3. **Verify Server Status**
|
|
||||||
- Check the server status in the Unity MCP window. It will display:
|
|
||||||
- **Unity Bridge**: Should show "Running" when active.
|
|
||||||
- **Python Server**: Should show "Connected" (green) when successfully linked.
|
|
||||||
|
|
||||||
## Manual Configuration for MCP Clients
|
|
||||||
|
|
||||||
If you prefer to manually configure your MCP client (like Claude Desktop or Cursor), you can create the configuration file yourself:
|
|
||||||
|
|
||||||
1. **Locate the Configuration Directory**
|
|
||||||
|
|
||||||
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
||||||
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
||||||
|
|
||||||
2. **Create the Configuration File**
|
|
||||||
Create a JSON file with the following structure:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"unityMCP": {
|
|
||||||
"command": "uv",
|
|
||||||
"args": [
|
|
||||||
"--directory",
|
|
||||||
"/path/to/your/unity-mcp/Python",
|
|
||||||
"run",
|
|
||||||
"server.py"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Find the Correct Python Path**
|
|
||||||
|
|
||||||
- If installed as a package: Look in `Library/PackageCache/com.justinpbarnett.unity-mcp/Python`
|
|
||||||
- If installed locally: Look in `Assets/unity-mcp/Python`
|
|
||||||
|
|
||||||
4. **Verify Configuration**
|
|
||||||
- Ensure the Python path points to the correct directory containing `server.py`
|
|
||||||
- Make sure the `uv` command is available in your system PATH
|
|
||||||
- Test the connection using the Unity MCP window
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Once configured, you can use the MCP Server to interact with LLMs directly from Unity or Python. Here are a couple of examples:
|
Once configured, you can use the MCP Client to interact with Unity directly through their chat interface.
|
||||||
|
|
||||||
### Creating a Cube in the Scene
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Send a command to create a cube at position (0, 0, 0)
|
|
||||||
create_primitive(primitive_type="Cube", position=[0, 0, 0])
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changing a Material's Color
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Set a material's color to red (RGBA: 1, 0, 0, 1)
|
|
||||||
set_material_color(material_name="MyMaterial", color=[1, 0, 0, 1])
|
|
||||||
```
|
|
||||||
|
|
||||||
Explore more commands in the [HOW_TO_ADD_A_TOOL.md](HOW_TO_ADD_A_TOOL.md) file for detailed examples and instructions on extending functionality.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
@ -205,7 +121,7 @@ Explore more commands in the [HOW_TO_ADD_A_TOOL.md](HOW_TO_ADD_A_TOOL.md) file f
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
We'd love your help to make the Unity MCP Server even better! Here's how to contribute:
|
I'd love your help to make the Unity MCP Server even better! Here's how to contribute:
|
||||||
|
|
||||||
1. **Fork the Repository**
|
1. **Fork the Repository**
|
||||||
Fork [github.com/justinpbarnett/unity-mcp](https://github.com/justinpbarnett/unity-mcp) to your GitHub account.
|
Fork [github.com/justinpbarnett/unity-mcp](https://github.com/justinpbarnett/unity-mcp) to your GitHub account.
|
||||||
|
|
@ -216,8 +132,14 @@ We'd love your help to make the Unity MCP Server even better! Here's how to cont
|
||||||
git checkout -b feature/your-feature-name
|
git checkout -b feature/your-feature-name
|
||||||
```
|
```
|
||||||
|
|
||||||
|
OR
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b bugfix/your-bugfix-name
|
||||||
|
```
|
||||||
|
|
||||||
3. **Make Changes**
|
3. **Make Changes**
|
||||||
Implement your feature or fix, following the project's coding standards (see [HOW_TO_ADD_A_TOOL.md](HOW_TO_ADD_A_TOOL.md) for guidance).
|
Implement your feature or fix.
|
||||||
|
|
||||||
4. **Commit and Push**
|
4. **Commit and Push**
|
||||||
Use clear, descriptive commit messages:
|
Use clear, descriptive commit messages:
|
||||||
|
|
@ -230,8 +152,6 @@ We'd love your help to make the Unity MCP Server even better! Here's how to cont
|
||||||
5. **Submit a Pull Request**
|
5. **Submit a Pull Request**
|
||||||
Open a pull request to the `master` branch. Include a description of your changes and any relevant details.
|
Open a pull request to the `master` branch. Include a description of your changes and any relevant details.
|
||||||
|
|
||||||
For more details, check out [CONTRIBUTING.md](CONTRIBUTING.md) (to be created).
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the **MIT License**. Feel free to use, modify, and distribute it as you see fit. See the full license [here](https://github.com/justinpbarnett/unity-mcp/blob/master/LICENSE).
|
This project is licensed under the **MIT License**. Feel free to use, modify, and distribute it as you see fit. See the full license [here](https://github.com/justinpbarnett/unity-mcp/blob/master/LICENSE).
|
||||||
|
|
@ -259,11 +179,9 @@ For additional help, check the [issue tracker](https://github.com/justinpbarnett
|
||||||
Have questions or want to chat about the project? Reach out!
|
Have questions or want to chat about the project? Reach out!
|
||||||
|
|
||||||
- **X**: [@justinpbarnett](https://x.com/justinpbarnett)
|
- **X**: [@justinpbarnett](https://x.com/justinpbarnett)
|
||||||
- **GitHub**: [justinpbarnett](https://github.com/justinpbarnett)
|
|
||||||
- **Discord**: Join our community (link coming soon!).
|
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
A huge thanks to everyone who's supported this project's initial release. Special shoutout to Unity Technologies for inspiring tools that push creative boundaries, and to the open-source community for making projects like this possible.
|
A huge thanks to everyone who's supported this project's initial release. Special shoutout to Unity Technologies for having an excellent Editor API.
|
||||||
|
|
||||||
Happy coding, and enjoy integrating LLMs with Unity!
|
Happy coding, and enjoy integrating LLMs with Unity!
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "com.justinpbarnett.unity-mcp",
|
"name": "com.justinpbarnett.unity-mcp",
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"displayName": "Unity MCP Server",
|
"displayName": "Unity MCP",
|
||||||
"description": "A Unity package to communicate with a local MCP Client via a Python server.",
|
"description": "A Unity package to communicate with a local MCP Client via a Python server.",
|
||||||
"unity": "2022.3",
|
"unity": "2022.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue