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 System.IO;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Collections.Generic;
namespace MCPServer.Editor.Commands
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Handles asset-related commands for the MCP Server
@ -51,7 +50,8 @@ namespace MCPServer.Editor.Commands
}
catch (System.Exception e)
{
return new {
return new
{
success = false,
error = $"Failed to import asset: {e.Message}",
stackTrace = e.StackTrace
@ -71,12 +71,12 @@ namespace MCPServer.Editor.Commands
if (string.IsNullOrEmpty(prefabPath))
return new { success = false, error = "Prefab path cannot be empty" };
Vector3 position = new Vector3(
Vector3 position = new(
(float)@params["position_x"],
(float)@params["position_y"],
(float)@params["position_z"]
);
Vector3 rotation = new Vector3(
Vector3 rotation = new(
(float)@params["rotation_x"],
(float)@params["rotation_y"],
(float)@params["rotation_z"]
@ -106,7 +106,8 @@ namespace MCPServer.Editor.Commands
}
catch (System.Exception e)
{
return new {
return new
{
success = false,
error = $"Failed to instantiate prefab: {e.Message}",
stackTrace = e.StackTrace
@ -162,7 +163,8 @@ namespace MCPServer.Editor.Commands
}
catch (System.Exception e)
{
return new {
return new
{
success = false,
error = $"Failed to create prefab: {e.Message}",
stackTrace = e.StackTrace,
@ -218,9 +220,9 @@ namespace MCPServer.Editor.Commands
assets.Add(new
{
name = Path.GetFileNameWithoutExtension(path),
path = path,
path,
type = assetType?.Name ?? "Unknown",
guid = guid
guid
});
}

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace MCPServer.Editor.Commands
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Registry for all MCP command handlers

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ using UnityEngine.Rendering;
using UnityEditor;
using System.IO;
namespace MCPServer.Editor.Commands
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Handles material-related commands
@ -70,7 +70,7 @@ namespace MCPServer.Editor.Commands
if (colorArray.Count < 3 || colorArray.Count > 4)
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[1],
(float)colorArray[2],

View File

@ -1,14 +1,14 @@
using UnityEngine;
using Newtonsoft.Json.Linq;
using MCPServer.Editor.Helpers;
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;
using UnityMCP.Editor.Helpers;
namespace MCPServer.Editor.Commands
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Handles object-related commands
@ -20,11 +20,11 @@ namespace MCPServer.Editor.Commands
/// </summary>
public static object GetObjectInfo(JObject @params)
{
string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found.");
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
return new
{
name = obj.name,
obj.name,
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 },
scale = new[] { obj.transform.localScale.x, obj.transform.localScale.y, obj.transform.localScale.z }
@ -36,7 +36,7 @@ namespace MCPServer.Editor.Commands
/// </summary>
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
{
"CUBE" => GameObject.CreatePrimitive(PrimitiveType.Cube),
@ -48,7 +48,7 @@ namespace MCPServer.Editor.Commands
"CAMERA" => new GameObject("Camera") { }.AddComponent<Camera>().gameObject,
"LIGHT" => new GameObject("Light") { }.AddComponent<Light>().gameObject,
"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"];
@ -56,7 +56,7 @@ namespace MCPServer.Editor.Commands
if (@params.ContainsKey("rotation")) obj.transform.eulerAngles = Vector3Helper.ParseVector3((JArray)@params["rotation"]);
if (@params.ContainsKey("scale")) obj.transform.localScale = Vector3Helper.ParseVector3((JArray)@params["scale"]);
return new { name = obj.name };
return new { obj.name };
}
/// <summary>
@ -64,8 +64,8 @@ namespace MCPServer.Editor.Commands
/// </summary>
public static object ModifyObject(JObject @params)
{
string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found.");
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
// Handle basic transform properties
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"))
{
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);
}
@ -109,7 +109,7 @@ namespace MCPServer.Editor.Commands
"TextMeshProUGUI" => typeof(TMPro.TextMeshProUGUI),
_ => Type.GetType($"UnityEngine.{componentType}") ??
Type.GetType(componentType) ??
throw new System.Exception($"Component type '{componentType}' not found.")
throw new Exception($"Component type '{componentType}' not found.")
};
obj.AddComponent(type);
}
@ -119,7 +119,7 @@ namespace MCPServer.Editor.Commands
string componentType = (string)@params["remove_component"];
Type type = Type.GetType($"UnityEngine.{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);
if (component != null)
UnityEngine.Object.DestroyImmediate(component);
@ -137,12 +137,12 @@ namespace MCPServer.Editor.Commands
if (componentType == "GameObject")
{
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
object gameObjectValue = Convert.ChangeType(value, gameObjectProperty.PropertyType);
gameObjectProperty.SetValue(obj, gameObjectValue);
return new { name = obj.name };
return new { obj.name };
}
// Handle component properties
@ -170,21 +170,21 @@ namespace MCPServer.Editor.Commands
"TextMeshProUGUI" => typeof(TMPro.TextMeshProUGUI),
_ => Type.GetType($"UnityEngine.{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) ??
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) ??
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
object propertyValue = Convert.ChangeType(value, property.PropertyType);
property.SetValue(component, propertyValue);
}
return new { name = obj.name };
return new { obj.name };
}
/// <summary>
@ -192,8 +192,8 @@ namespace MCPServer.Editor.Commands
/// </summary>
public static object DeleteObject(JObject @params)
{
string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found.");
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
UnityEngine.Object.DestroyImmediate(obj);
return new { name };
}
@ -203,8 +203,8 @@ namespace MCPServer.Editor.Commands
/// </summary>
public static object GetObjectProperties(JObject @params)
{
string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found.");
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
var components = obj.GetComponents<Component>()
.Select(c => new
@ -216,9 +216,9 @@ namespace MCPServer.Editor.Commands
return new
{
name = obj.name,
tag = obj.tag,
layer = obj.layer,
obj.name,
obj.tag,
obj.layer,
active = obj.activeSelf,
transform = new
{
@ -235,11 +235,11 @@ namespace MCPServer.Editor.Commands
/// </summary>
public static object GetComponentProperties(JObject @params)
{
string objectName = (string)@params["object_name"] ?? throw new System.Exception("Parameter 'object_name' is required.");
string componentType = (string)@params["component_type"] ?? throw new System.Exception("Parameter 'component_type' is required.");
string objectName = (string)@params["object_name"] ?? throw new Exception("Parameter 'object_name' 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 component = obj.GetComponent(componentType) ?? throw new System.Exception($"Component '{componentType}' not found on object '{objectName}'.");
var obj = GameObject.Find(objectName) ?? throw new Exception($"Object '{objectName}' not found.");
var component = obj.GetComponent(componentType) ?? throw new Exception($"Component '{componentType}' not found on object '{objectName}'.");
return GetComponentProperties(component);
}
@ -249,12 +249,12 @@ namespace MCPServer.Editor.Commands
/// </summary>
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)
.Where(o => o.name.Contains(name))
.Select(o => new
{
name = o.name,
o.name,
path = GetGameObjectPath(o)
})
.ToList();
@ -267,11 +267,11 @@ namespace MCPServer.Editor.Commands
/// </summary>
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)
.Select(o => new
{
name = o.name,
o.name,
path = GetGameObjectPath(o)
})
.ToList();
@ -295,11 +295,11 @@ namespace MCPServer.Editor.Commands
/// </summary>
public static object SelectObject(JObject @params)
{
string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new System.Exception($"Object '{name}' not found.");
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
Selection.activeGameObject = obj;
return new { name = obj.name };
return new { obj.name };
}
/// <summary>
@ -315,7 +315,7 @@ namespace MCPServer.Editor.Commands
{
selected = new
{
name = selected.name,
selected.name,
path = GetGameObjectPath(selected)
}
};
@ -379,7 +379,7 @@ namespace MCPServer.Editor.Commands
{
return new
{
name = obj.name,
obj.name,
children = Enumerable.Range(0, obj.transform.childCount)
.Select(i => BuildHierarchyNode(obj.transform.GetChild(i).gameObject))
.ToList()

View File

@ -1,11 +1,11 @@
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Linq;
using System;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
namespace MCPServer.Editor.Commands
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Handles scene-related commands for the MCP Server
@ -42,7 +42,7 @@ namespace MCPServer.Editor.Commands
EditorSceneManager.OpenScene(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 };
}
@ -60,7 +60,7 @@ namespace MCPServer.Editor.Commands
EditorSceneManager.SaveScene(scene);
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 };
}
@ -96,7 +96,7 @@ namespace MCPServer.Editor.Commands
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 };
}
@ -131,7 +131,7 @@ namespace MCPServer.Editor.Commands
EditorSceneManager.OpenScene(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 };
}

View File

@ -5,9 +5,8 @@ using System.IO;
using System.Text;
using System.Linq;
using Newtonsoft.Json.Linq;
using MCPServer.Editor.Helpers;
namespace MCPServer.Editor.Commands
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Handles script-related commands for Unity
@ -19,12 +18,9 @@ namespace MCPServer.Editor.Commands
/// </summary>
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;
// Debug to help diagnose issues
Debug.Log($"ViewScript - Original script path: {scriptPath}");
// Handle path correctly to avoid double "Assets" folder issue
string relativePath;
if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
@ -38,14 +34,12 @@ namespace MCPServer.Editor.Commands
}
string fullPath = Path.Combine(Application.dataPath, relativePath);
Debug.Log($"ViewScript - Relative path: {relativePath}");
Debug.Log($"ViewScript - Full path: {fullPath}");
if (!File.Exists(fullPath))
{
if (requireExists)
{
throw new System.Exception($"Script file not found: {scriptPath}");
throw new Exception($"Script file not found: {scriptPath}");
}
else
{
@ -76,7 +70,7 @@ namespace MCPServer.Editor.Commands
/// </summary>
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 namespaceName = (string)@params["namespace"];
string template = (string)@params["template"];
@ -115,9 +109,6 @@ namespace MCPServer.Editor.Commands
// Create the full directory path, avoiding Assets/Assets issue
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
if (!Directory.Exists(folderPath))
@ -129,7 +120,7 @@ namespace MCPServer.Editor.Commands
}
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);
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
@ -151,7 +142,7 @@ namespace MCPServer.Editor.Commands
else
{
// Otherwise generate content based on template and parameters
StringBuilder contentBuilder = new StringBuilder();
StringBuilder contentBuilder = new();
// Add using directives
contentBuilder.AppendLine("using UnityEngine;");
@ -213,7 +204,8 @@ namespace MCPServer.Editor.Commands
relativePath = $"Assets/{relativePath}";
}
return new {
return new
{
message = $"Created script: {Path.Combine(relativePath, scriptName).Replace('\\', '/')}",
script_path = Path.Combine(relativePath, scriptName).Replace('\\', '/')
};
@ -221,7 +213,7 @@ namespace MCPServer.Editor.Commands
catch (Exception ex)
{
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>
public static object UpdateScript(JObject @params)
{
string scriptPath = (string)@params["script_path"] ?? throw new System.Exception("Parameter 'script_path' is required.");
string content = (string)@params["content"] ?? throw new System.Exception("Parameter 'content' is required.");
string scriptPath = (string)@params["script_path"] ?? throw new Exception("Parameter 'script_path' is required.");
string content = (string)@params["content"] ?? throw new Exception("Parameter 'content' is required.");
bool createIfMissing = (bool?)@params["create_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);
// 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
if (!File.Exists(fullPath))
@ -267,7 +257,7 @@ namespace MCPServer.Editor.Commands
}
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
@ -277,7 +267,7 @@ namespace MCPServer.Editor.Commands
}
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))
throw new System.Exception($"Folder not found: {folderPath}");
throw new Exception($"Folder not found: {folderPath}");
string[] scripts = Directory.GetFiles(fullPath, "*.cs", SearchOption.AllDirectories)
.Select(path => path.Replace(Application.dataPath, "Assets"))
@ -330,14 +320,14 @@ namespace MCPServer.Editor.Commands
/// </summary>
public static object AttachScript(JObject @params)
{
string objectName = (string)@params["object_name"] ?? throw new System.Exception("Parameter 'object_name' is required.");
string scriptName = (string)@params["script_name"] ?? throw new System.Exception("Parameter 'script_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 Exception("Parameter 'script_name' is required.");
string scriptPath = (string)@params["script_path"]; // Optional
// Find the target object
GameObject targetObject = GameObject.Find(objectName);
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
if (!scriptName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
@ -359,7 +349,7 @@ namespace MCPServer.Editor.Commands
MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptPath);
if (scriptAsset != null)
{
System.Type scriptType = scriptAsset.GetClass();
Type scriptType = scriptAsset.GetClass();
if (scriptType != null)
{
try
@ -378,7 +368,7 @@ namespace MCPServer.Editor.Commands
catch (Exception ex)
{
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);
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
@ -416,7 +406,7 @@ namespace MCPServer.Editor.Commands
if (scriptAsset == null)
continue;
System.Type scriptType = scriptAsset.GetClass();
Type scriptType = scriptAsset.GetClass();
if (scriptType == null || !typeof(MonoBehaviour).IsAssignableFrom(scriptType))
continue;
@ -452,7 +442,7 @@ namespace MCPServer.Editor.Commands
}
// 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 Newtonsoft.Json.Linq;
namespace MCPServer.Editor.Helpers
namespace UnityMCP.Editor.Helpers
{
/// <summary>
/// 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;
namespace MCPServer.Editor.Models
namespace UnityMCP.Editor.Models
{
/// <summary>
/// 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,344 +3,343 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using MCPServer.Editor.Models;
using MCPServer.Editor.Commands;
using MCPServer.Editor.Helpers;
using System.IO;
using UnityMCP.Editor.Models;
using UnityMCP.Editor.Commands;
[InitializeOnLoad]
public static partial class UnityMCPBridge
namespace UnityMCP.Editor
{
private static TcpListener listener;
private static bool isRunning = false;
private static readonly object lockObj = new object();
private static Dictionary<string, (string commandJson, TaskCompletionSource<string> tcs)> commandQueue = new();
private static readonly int unityPort = 6400; // Hardcoded port
// Add public property to expose running state
public static bool IsRunning => isRunning;
// Add method to check existence of a folder
public static bool FolderExists(string path)
[InitializeOnLoad]
public static partial class UnityMCPBridge
{
if (string.IsNullOrEmpty(path))
return false;
private static TcpListener listener;
private static bool isRunning = false;
private static readonly object lockObj = new();
private static Dictionary<string, (string commandJson, TaskCompletionSource<string> tcs)> commandQueue = new();
private static readonly int unityPort = 6400; // Hardcoded port
if (path.Equals("Assets", StringComparison.OrdinalIgnoreCase))
return true;
public static bool IsRunning => isRunning;
string fullPath = Path.Combine(Application.dataPath, path.StartsWith("Assets/") ? path.Substring(7) : path);
return Directory.Exists(fullPath);
}
static UnityMCPBridge()
{
Start();
EditorApplication.quitting += Stop;
}
public static void Start()
{
if (isRunning) return;
isRunning = true;
listener = new TcpListener(IPAddress.Loopback, unityPort);
listener.Start();
Debug.Log($"UnityMCPBridge started on port {unityPort}.");
Task.Run(ListenerLoop);
EditorApplication.update += ProcessCommands;
}
public static void Stop()
{
if (!isRunning) return;
isRunning = false;
listener.Stop();
EditorApplication.update -= ProcessCommands;
Debug.Log("UnityMCPBridge stopped.");
}
private static async Task ListenerLoop()
{
while (isRunning)
public static bool FolderExists(string path)
{
try
{
var client = await listener.AcceptTcpClientAsync();
// Enable basic socket keepalive
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
if (string.IsNullOrEmpty(path))
return false;
// Set longer receive timeout to prevent quick disconnections
client.ReceiveTimeout = 60000; // 60 seconds
if (path.Equals("Assets", StringComparison.OrdinalIgnoreCase))
return true;
// Fire and forget each client connection
_ = HandleClientAsync(client);
}
catch (Exception ex)
{
if (isRunning) Debug.LogError($"Listener error: {ex.Message}");
}
string fullPath = Path.Combine(Application.dataPath, path.StartsWith("Assets/") ? path.Substring(7) : path);
return Directory.Exists(fullPath);
}
}
private static async Task HandleClientAsync(TcpClient client)
{
using (client)
using (var stream = client.GetStream())
static UnityMCPBridge()
{
Start();
EditorApplication.quitting += Stop;
}
public static void Start()
{
if (isRunning) return;
isRunning = true;
listener = new TcpListener(IPAddress.Loopback, unityPort);
listener.Start();
Debug.Log($"UnityMCPBridge started on port {unityPort}.");
Task.Run(ListenerLoop);
EditorApplication.update += ProcessCommands;
}
public static void Stop()
{
if (!isRunning) return;
isRunning = false;
listener.Stop();
EditorApplication.update -= ProcessCommands;
Debug.Log("UnityMCPBridge stopped.");
}
private static async Task ListenerLoop()
{
var buffer = new byte[8192];
while (isRunning)
{
try
{
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0) break; // Client disconnected
var client = await listener.AcceptTcpClientAsync();
// Enable basic socket keepalive
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
string commandText = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);
string commandId = Guid.NewGuid().ToString();
var tcs = new TaskCompletionSource<string>();
// Set longer receive timeout to prevent quick disconnections
client.ReceiveTimeout = 60000; // 60 seconds
// Special handling for ping command to avoid JSON parsing
if (commandText.Trim() == "ping")
{
// Direct response to ping without going through JSON parsing
byte[] pingResponseBytes = System.Text.Encoding.UTF8.GetBytes("{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}");
await stream.WriteAsync(pingResponseBytes, 0, pingResponseBytes.Length);
continue;
}
lock (lockObj)
{
commandQueue[commandId] = (commandText, tcs);
}
string response = await tcs.Task;
byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
// Fire and forget each client connection
_ = HandleClientAsync(client);
}
catch (Exception ex)
{
Debug.LogError($"Client handler error: {ex.Message}");
break;
if (isRunning) Debug.LogError($"Listener error: {ex.Message}");
}
}
}
}
private static void ProcessCommands()
{
List<string> processedIds = new();
lock (lockObj)
private static async Task HandleClientAsync(TcpClient client)
{
foreach (var kvp in commandQueue.ToList())
using (client)
using (var stream = client.GetStream())
{
string id = kvp.Key;
string commandText = kvp.Value.commandJson;
var tcs = kvp.Value.tcs;
try
var buffer = new byte[8192];
while (isRunning)
{
// Special case handling
if (string.IsNullOrEmpty(commandText))
try
{
var emptyResponse = new
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0) break; // Client disconnected
string commandText = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);
string commandId = Guid.NewGuid().ToString();
var tcs = new TaskCompletionSource<string>();
// Special handling for ping command to avoid JSON parsing
if (commandText.Trim() == "ping")
{
// Direct response to ping without going through JSON parsing
byte[] pingResponseBytes = System.Text.Encoding.UTF8.GetBytes("{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}");
await stream.WriteAsync(pingResponseBytes, 0, pingResponseBytes.Length);
continue;
}
lock (lockObj)
{
commandQueue[commandId] = (commandText, tcs);
}
string response = await tcs.Task;
byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
}
catch (Exception ex)
{
Debug.LogError($"Client handler error: {ex.Message}");
break;
}
}
}
}
private static void ProcessCommands()
{
List<string> processedIds = new();
lock (lockObj)
{
foreach (var kvp in commandQueue.ToList())
{
string id = kvp.Key;
string commandText = kvp.Value.commandJson;
var tcs = kvp.Value.tcs;
try
{
// Special case handling
if (string.IsNullOrEmpty(commandText))
{
var emptyResponse = new
{
status = "error",
error = "Empty command received"
};
tcs.SetResult(JsonConvert.SerializeObject(emptyResponse));
processedIds.Add(id);
continue;
}
// Trim the command text to remove any whitespace
commandText = commandText.Trim();
// Non-JSON direct commands handling (like ping)
if (commandText == "ping")
{
var pingResponse = new
{
status = "success",
result = new { message = "pong" }
};
tcs.SetResult(JsonConvert.SerializeObject(pingResponse));
processedIds.Add(id);
continue;
}
// Check if the command is valid JSON before attempting to deserialize
if (!IsValidJson(commandText))
{
var invalidJsonResponse = new
{
status = "error",
error = "Invalid JSON format",
receivedText = commandText.Length > 50 ? commandText.Substring(0, 50) + "..." : commandText
};
tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse));
processedIds.Add(id);
continue;
}
// Normal JSON command processing
var command = JsonConvert.DeserializeObject<Command>(commandText);
if (command == null)
{
var nullCommandResponse = new
{
status = "error",
error = "Command deserialized to null",
details = "The command was valid JSON but could not be deserialized to a Command object"
};
tcs.SetResult(JsonConvert.SerializeObject(nullCommandResponse));
}
else
{
string responseJson = ExecuteCommand(command);
tcs.SetResult(responseJson);
}
}
catch (Exception ex)
{
Debug.LogError($"Error processing command: {ex.Message}\n{ex.StackTrace}");
var response = new
{
status = "error",
error = "Empty command received"
error = ex.Message,
commandType = "Unknown (error during processing)",
receivedText = commandText?.Length > 50 ? commandText.Substring(0, 50) + "..." : commandText
};
tcs.SetResult(JsonConvert.SerializeObject(emptyResponse));
processedIds.Add(id);
continue;
}
// Trim the command text to remove any whitespace
commandText = commandText.Trim();
// Non-JSON direct commands handling (like ping)
if (commandText == "ping")
{
var pingResponse = new
{
status = "success",
result = new { message = "pong" }
};
tcs.SetResult(JsonConvert.SerializeObject(pingResponse));
processedIds.Add(id);
continue;
}
// Check if the command is valid JSON before attempting to deserialize
if (!IsValidJson(commandText))
{
var invalidJsonResponse = new
{
status = "error",
error = "Invalid JSON format",
receivedText = commandText.Length > 50 ? commandText.Substring(0, 50) + "..." : commandText
};
tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse));
processedIds.Add(id);
continue;
}
// Normal JSON command processing
var command = JsonConvert.DeserializeObject<Command>(commandText);
if (command == null)
{
var nullCommandResponse = new
{
status = "error",
error = "Command deserialized to null",
details = "The command was valid JSON but could not be deserialized to a Command object"
};
tcs.SetResult(JsonConvert.SerializeObject(nullCommandResponse));
}
else
{
string responseJson = ExecuteCommand(command);
string responseJson = JsonConvert.SerializeObject(response);
tcs.SetResult(responseJson);
}
processedIds.Add(id);
}
catch (Exception ex)
foreach (var id in processedIds)
{
Debug.LogError($"Error processing command: {ex.Message}\n{ex.StackTrace}");
var response = new
{
status = "error",
error = ex.Message,
commandType = "Unknown (error during processing)",
receivedText = commandText?.Length > 50 ? commandText.Substring(0, 50) + "..." : commandText
};
string responseJson = JsonConvert.SerializeObject(response);
tcs.SetResult(responseJson);
commandQueue.Remove(id);
}
processedIds.Add(id);
}
foreach (var id in processedIds)
{
commandQueue.Remove(id);
}
}
}
// Helper method to check if a string is valid JSON
private static bool IsValidJson(string text)
{
if (string.IsNullOrWhiteSpace(text))
// Helper method to check if a string is valid JSON
private static bool IsValidJson(string text)
{
if (string.IsNullOrWhiteSpace(text))
return false;
text = text.Trim();
if ((text.StartsWith("{") && text.EndsWith("}")) || // Object
(text.StartsWith("[") && text.EndsWith("]"))) // Array
{
try
{
JToken.Parse(text);
return true;
}
catch
{
return false;
}
}
return false;
}
text = text.Trim();
if ((text.StartsWith("{") && text.EndsWith("}")) || // Object
(text.StartsWith("[") && text.EndsWith("]"))) // Array
private static string ExecuteCommand(Command command)
{
try
{
JToken.Parse(text);
return true;
if (string.IsNullOrEmpty(command.type))
{
var errorResponse = new
{
status = "error",
error = "Command type cannot be empty",
details = "A valid command type is required for processing"
};
return JsonConvert.SerializeObject(errorResponse);
}
// Handle ping command for connection verification
if (command.type == "ping")
{
var pingResponse = new { status = "success", result = new { message = "pong" } };
return JsonConvert.SerializeObject(pingResponse);
}
object result = command.type switch
{
"GET_SCENE_INFO" => SceneCommandHandler.GetSceneInfo(),
"OPEN_SCENE" => SceneCommandHandler.OpenScene(command.@params),
"SAVE_SCENE" => SceneCommandHandler.SaveScene(),
"NEW_SCENE" => SceneCommandHandler.NewScene(command.@params),
"CHANGE_SCENE" => SceneCommandHandler.ChangeScene(command.@params),
"GET_OBJECT_INFO" => ObjectCommandHandler.GetObjectInfo(command.@params),
"CREATE_OBJECT" => ObjectCommandHandler.CreateObject(command.@params),
"MODIFY_OBJECT" => ObjectCommandHandler.ModifyObject(command.@params),
"DELETE_OBJECT" => ObjectCommandHandler.DeleteObject(command.@params),
"GET_OBJECT_PROPERTIES" => ObjectCommandHandler.GetObjectProperties(command.@params),
"GET_COMPONENT_PROPERTIES" => ObjectCommandHandler.GetComponentProperties(command.@params),
"FIND_OBJECTS_BY_NAME" => ObjectCommandHandler.FindObjectsByName(command.@params),
"FIND_OBJECTS_BY_TAG" => ObjectCommandHandler.FindObjectsByTag(command.@params),
"GET_HIERARCHY" => ObjectCommandHandler.GetHierarchy(),
"SELECT_OBJECT" => ObjectCommandHandler.SelectObject(command.@params),
"GET_SELECTED_OBJECT" => ObjectCommandHandler.GetSelectedObject(),
"SET_MATERIAL" => MaterialCommandHandler.SetMaterial(command.@params),
"VIEW_SCRIPT" => ScriptCommandHandler.ViewScript(command.@params),
"CREATE_SCRIPT" => ScriptCommandHandler.CreateScript(command.@params),
"UPDATE_SCRIPT" => ScriptCommandHandler.UpdateScript(command.@params),
"LIST_SCRIPTS" => ScriptCommandHandler.ListScripts(command.@params),
"ATTACH_SCRIPT" => ScriptCommandHandler.AttachScript(command.@params),
"IMPORT_ASSET" => AssetCommandHandler.ImportAsset(command.@params),
"INSTANTIATE_PREFAB" => AssetCommandHandler.InstantiatePrefab(command.@params),
"CREATE_PREFAB" => AssetCommandHandler.CreatePrefab(command.@params),
"APPLY_PREFAB" => AssetCommandHandler.ApplyPrefab(command.@params),
"GET_ASSET_LIST" => AssetCommandHandler.GetAssetList(command.@params),
"EDITOR_CONTROL" => EditorControlHandler.HandleEditorControl(command.@params),
_ => throw new Exception($"Unknown command type: {command.type}")
};
var response = new { status = "success", result };
return JsonConvert.SerializeObject(response);
}
catch (Exception ex)
{
Debug.LogError($"Error executing command {command.type}: {ex.Message}\n{ex.StackTrace}");
var response = new
{
status = "error",
error = ex.Message,
command = command.type,
stackTrace = ex.StackTrace,
paramsSummary = command.@params != null ? GetParamsSummary(command.@params) : "No parameters"
};
return JsonConvert.SerializeObject(response);
}
}
// Helper method to get a summary of parameters for error reporting
private static string GetParamsSummary(JObject @params)
{
try
{
if (@params == null || !@params.HasValues)
return "No parameters";
return string.Join(", ", @params.Properties().Select(p => $"{p.Name}: {p.Value?.ToString()?.Substring(0, Math.Min(20, p.Value?.ToString()?.Length ?? 0))}"));
}
catch
{
return false;
return "Could not summarize parameters";
}
}
return false;
}
private static string ExecuteCommand(Command command)
{
try
{
if (string.IsNullOrEmpty(command.type))
{
var errorResponse = new
{
status = "error",
error = "Command type cannot be empty",
details = "A valid command type is required for processing"
};
return JsonConvert.SerializeObject(errorResponse);
}
// Handle ping command for connection verification
if (command.type == "ping")
{
var pingResponse = new { status = "success", result = new { message = "pong" } };
return JsonConvert.SerializeObject(pingResponse);
}
object result = command.type switch
{
"GET_SCENE_INFO" => SceneCommandHandler.GetSceneInfo(),
"OPEN_SCENE" => SceneCommandHandler.OpenScene(command.@params),
"SAVE_SCENE" => SceneCommandHandler.SaveScene(),
"NEW_SCENE" => SceneCommandHandler.NewScene(command.@params),
"CHANGE_SCENE" => SceneCommandHandler.ChangeScene(command.@params),
"GET_OBJECT_INFO" => ObjectCommandHandler.GetObjectInfo(command.@params),
"CREATE_OBJECT" => ObjectCommandHandler.CreateObject(command.@params),
"MODIFY_OBJECT" => ObjectCommandHandler.ModifyObject(command.@params),
"DELETE_OBJECT" => ObjectCommandHandler.DeleteObject(command.@params),
"GET_OBJECT_PROPERTIES" => ObjectCommandHandler.GetObjectProperties(command.@params),
"GET_COMPONENT_PROPERTIES" => ObjectCommandHandler.GetComponentProperties(command.@params),
"FIND_OBJECTS_BY_NAME" => ObjectCommandHandler.FindObjectsByName(command.@params),
"FIND_OBJECTS_BY_TAG" => ObjectCommandHandler.FindObjectsByTag(command.@params),
"GET_HIERARCHY" => ObjectCommandHandler.GetHierarchy(),
"SELECT_OBJECT" => ObjectCommandHandler.SelectObject(command.@params),
"GET_SELECTED_OBJECT" => ObjectCommandHandler.GetSelectedObject(),
"SET_MATERIAL" => MaterialCommandHandler.SetMaterial(command.@params),
"VIEW_SCRIPT" => ScriptCommandHandler.ViewScript(command.@params),
"CREATE_SCRIPT" => ScriptCommandHandler.CreateScript(command.@params),
"UPDATE_SCRIPT" => ScriptCommandHandler.UpdateScript(command.@params),
"LIST_SCRIPTS" => ScriptCommandHandler.ListScripts(command.@params),
"ATTACH_SCRIPT" => ScriptCommandHandler.AttachScript(command.@params),
"IMPORT_ASSET" => AssetCommandHandler.ImportAsset(command.@params),
"INSTANTIATE_PREFAB" => AssetCommandHandler.InstantiatePrefab(command.@params),
"CREATE_PREFAB" => AssetCommandHandler.CreatePrefab(command.@params),
"APPLY_PREFAB" => AssetCommandHandler.ApplyPrefab(command.@params),
"GET_ASSET_LIST" => AssetCommandHandler.GetAssetList(command.@params),
"EDITOR_CONTROL" => EditorControlHandler.HandleEditorControl(command.@params),
_ => throw new Exception($"Unknown command type: {command.type}")
};
var response = new { status = "success", result };
return JsonConvert.SerializeObject(response);
}
catch (Exception ex)
{
Debug.LogError($"Error executing command {command.type}: {ex.Message}\n{ex.StackTrace}");
var response = new
{
status = "error",
error = ex.Message,
command = command.type,
stackTrace = ex.StackTrace,
paramsSummary = command.@params != null ? GetParamsSummary(command.@params) : "No parameters"
};
return JsonConvert.SerializeObject(response);
}
}
// Helper method to get a summary of parameters for error reporting
private static string GetParamsSummary(JObject @params)
{
try
{
if (@params == null || !@params.HasValues)
return "No parameters";
return string.Join(", ", @params.Properties().Select(p => $"{p.Name}: {p.Value?.ToString()?.Substring(0, Math.Min(20, p.Value?.ToString()?.Length ?? 0))}"));
}
catch
{
return "Could not summarize parameters";
}
}
}

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.
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
@ -20,7 +20,7 @@ This project is perfect for developers who want to leverage LLMs to enhance thei
### Prerequisites
- Unity 2020.3 LTS or newer
- Unity 2020.3 LTS or newer (⚠️ only works in URP projects currently)
- Python 3.7 or newer
- uv package manager
@ -70,16 +70,17 @@ Otherwise, installation instructions are on their website: [Install uv](https://
uv pip install -e .
```
### Claude Desktop Integration
### MCP Client Integration
1. Open the Unity MCP window (`Window > Unity MCP`)
2. Click the "Configure Claude" button
3. Follow the on-screen instructions to set up the integration
2. Click the "Auto Configure" button for your desired MCP client
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
2. Edit `claude_desktop_config.json` to include:
1. Open the Unity MCP window (`Window > Unity MCP`)
2. Click the "Manually Configure" button for your desired MCP client
3. Copy the JSON code below to the config file
```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.
### 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**
4. **Start Claude Desktop or Cursor**
- Launch your preferred tool
- The Unity MCP Server will automatically 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
- The Unity MCP Server will automatically start and connect
## Usage
Once configured, you can use the MCP Server to interact with LLMs directly from Unity or Python. Here are a couple of examples:
### 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.
Once configured, you can use the MCP Client to interact with Unity directly through their chat interface.
## 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
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**
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
```
OR
```bash
git checkout -b bugfix/your-bugfix-name
```
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**
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**
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
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!
- **X**: [@justinpbarnett](https://x.com/justinpbarnett)
- **GitHub**: [justinpbarnett](https://github.com/justinpbarnett)
- **Discord**: Join our community (link coming soon!).
## 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!