added cursor auto config

main
Justin Barnett 2025-03-20 07:24:31 -04:00
parent c2be2bfa34
commit 6b8cf0eab2
37 changed files with 2039 additions and 1627 deletions

View File

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

View File

@ -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

View File

@ -5,9 +5,10 @@ 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;
namespace UnityMCP.Editor.Commands
{
/// <summary> /// <summary>
/// Handles editor control commands like undo, redo, play, pause, stop, and build operations. /// Handles editor control commands like undo, redo, play, pause, stop, and build operations.
/// </summary> /// </summary>
@ -21,27 +22,18 @@ 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": _ => new { error = $"Unknown editor control command: {command}" },
return HandleStop(); };
case "BUILD":
return HandleBuild(commandParams);
case "EXECUTE_COMMAND":
return HandleExecuteCommand(commandParams);
case "READ_CONSOLE":
return ReadConsole(commandParams);
default:
return new { error = $"Unknown editor control command: {command}" };
}
} }
private static object HandleUndo() private static object HandleUndo()
@ -99,19 +91,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 return new
{ {
message = "Build completed successfully", message = "Build completed successfully",
summary = report.summary report.summary
}; };
} }
catch (System.Exception e) catch (Exception e)
{ {
return new { error = $"Build failed: {e.Message}" }; return new { error = $"Build failed: {e.Message}" };
} }
@ -125,7 +119,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}" };
} }
@ -383,8 +377,8 @@ public static class EditorControlHandler
entries.Add(new entries.Add(new
{ {
type = typeStr, type = typeStr,
message = message, message,
stackTrace = stackTrace stackTrace
}); });
} }
catch (Exception) catch (Exception)
@ -398,7 +392,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,
@ -446,7 +440,7 @@ public static class EditorControlHandler
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)
@ -590,3 +584,4 @@ public static class EditorControlHandler
return result; return result;
} }
} }
}

View File

@ -5,7 +5,7 @@ using UnityEngine.Rendering;
using UnityEditor; using UnityEditor;
using System.IO; using System.IO;
namespace MCPServer.Editor.Commands namespace UnityMCP.Editor.Commands
{ {
/// <summary> /// <summary>
/// Handles material-related commands /// Handles material-related commands
@ -70,7 +70,7 @@ namespace MCPServer.Editor.Commands
if (colorArray.Count < 3 || colorArray.Count > 4) if (colorArray.Count < 3 || colorArray.Count > 4)
throw new System.Exception("Color must be an array of 3 (RGB) or 4 (RGBA) floats."); throw new System.Exception("Color must be an array of 3 (RGB) or 4 (RGBA) floats.");
Color color = new Color( Color color = new(
(float)colorArray[0], (float)colorArray[0],
(float)colorArray[1], (float)colorArray[1],
(float)colorArray[2], (float)colorArray[2],

View File

@ -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()

View File

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

View File

@ -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.");
} }
} }
} }

8
Editor/Data.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e59036660cc33d24596fbbf6d4657a83
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: de8f5721c34f7194392e9d8c7d0226c0

57
Editor/Data/McpClients.cs Normal file
View File

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

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 711b86bbc1f661e4fb2c822e14970e16

View File

@ -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

View File

@ -1,622 +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}");
// Create configuration object
var config = new MCPConfig
{
mcpServers = new MCPConfigServers
{
unityMCP = new MCPConfigServer
{
command = "uv",
args = new[]
{
"--directory",
pythonDir,
"run",
"server.py"
}
}
}
};
// Serialize and write to file with proper formatting
var jsonSettings = new JsonSerializerSettings
{
Formatting = Formatting.Indented
};
string jsonConfig = JsonConvert.SerializeObject(config, 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();
}
}
}
}

View File

@ -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

View File

@ -0,0 +1,12 @@
using System;
using Newtonsoft.Json;
namespace UnityMCP.Editor.Models
{
[Serializable]
public class MCPConfig
{
[JsonProperty("mcpServers")]
public MCPConfigServers mcpServers;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c17c09908f0c1524daa8b6957ce1f7f5

View File

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

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5fae9d995f514e9498e9613e2cdbeca9

View File

@ -0,0 +1,12 @@
using System;
using Newtonsoft.Json;
namespace UnityMCP.Editor.Models
{
[Serializable]
public class MCPConfigServers
{
[JsonProperty("unityMCP")]
public MCPConfigServer unityMCP;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bcb583553e8173b49be71a5c43bd9502

View File

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

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b1afa56984aec0d41808edcebf805e6a

View File

@ -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
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: aa63057c9e5282d4887352578bf49971

View File

@ -0,0 +1,8 @@
namespace UnityMCP.Editor.Models
{
public enum McpTypes
{
ClaudeDesktop,
Cursor
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1

View File

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

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e4e45386fcc282249907c2e3c7e5d9c6

View File

@ -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;
namespace UnityMCP.Editor
{
[InitializeOnLoad] [InitializeOnLoad]
public static partial class UnityMCPBridge 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))
@ -344,3 +342,4 @@ public static partial class UnityMCPBridge
} }
} }
} }
}

8
Editor/Windows.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d2ee39f5d4171184eb208e865c1ef4c1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 36798bd7b867b8e43ac86885e94f928f

View File

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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 714de9c710feb1a42878a16b7a4e7a6f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

122
README.md
View File

@ -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!