initial commit

main
Justin Barnett 2025-03-18 07:00:50 -04:00
commit 276ac8166f
69 changed files with 4132 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
.codeignore
*codeclip*
# Python-generated files
__pycache__/
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

8
Editor.meta Normal file
View File

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

8
Editor/Commands.meta Normal file
View File

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

View File

@ -0,0 +1,230 @@
using UnityEngine;
using UnityEditor;
using System.IO;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Collections.Generic;
namespace MCPServer.Editor.Commands
{
/// <summary>
/// Handles asset-related commands for the MCP Server
/// </summary>
public static class AssetCommandHandler
{
/// <summary>
/// Imports an asset into the project
/// </summary>
public static object ImportAsset(JObject @params)
{
try
{
string sourcePath = (string)@params["source_path"];
string targetPath = (string)@params["target_path"];
if (string.IsNullOrEmpty(sourcePath))
return new { success = false, error = "Source path cannot be empty" };
if (string.IsNullOrEmpty(targetPath))
return new { success = false, error = "Target path cannot be empty" };
if (!File.Exists(sourcePath))
return new { success = false, error = $"Source file not found: {sourcePath}" };
// Ensure target directory exists
string targetDir = Path.GetDirectoryName(targetPath);
if (!Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
// Copy file to target location
File.Copy(sourcePath, targetPath, true);
AssetDatabase.Refresh();
return new
{
success = true,
message = $"Successfully imported asset to {targetPath}",
path = targetPath
};
}
catch (System.Exception e)
{
return new {
success = false,
error = $"Failed to import asset: {e.Message}",
stackTrace = e.StackTrace
};
}
}
/// <summary>
/// Instantiates a prefab in the current scene
/// </summary>
public static object InstantiatePrefab(JObject @params)
{
try
{
string prefabPath = (string)@params["prefab_path"];
if (string.IsNullOrEmpty(prefabPath))
return new { success = false, error = "Prefab path cannot be empty" };
Vector3 position = new Vector3(
(float)@params["position_x"],
(float)@params["position_y"],
(float)@params["position_z"]
);
Vector3 rotation = new Vector3(
(float)@params["rotation_x"],
(float)@params["rotation_y"],
(float)@params["rotation_z"]
);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
if (prefab == null)
{
return new { success = false, error = $"Prefab not found at path: {prefabPath}" };
}
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
if (instance == null)
{
return new { success = false, error = $"Failed to instantiate prefab: {prefabPath}" };
}
instance.transform.position = position;
instance.transform.rotation = Quaternion.Euler(rotation);
return new
{
success = true,
message = "Successfully instantiated prefab",
instance_name = instance.name
};
}
catch (System.Exception e)
{
return new {
success = false,
error = $"Failed to instantiate prefab: {e.Message}",
stackTrace = e.StackTrace
};
}
}
/// <summary>
/// Creates a new prefab from a GameObject in the scene
/// </summary>
public static object CreatePrefab(JObject @params)
{
try
{
string objectName = (string)@params["object_name"];
string prefabPath = (string)@params["prefab_path"];
if (string.IsNullOrEmpty(objectName))
return new { success = false, error = "GameObject name cannot be empty" };
if (string.IsNullOrEmpty(prefabPath))
return new { success = false, error = "Prefab path cannot be empty" };
// Ensure prefab has .prefab extension
if (!prefabPath.ToLower().EndsWith(".prefab"))
prefabPath = $"{prefabPath}.prefab";
GameObject sourceObject = GameObject.Find(objectName);
if (sourceObject == null)
{
return new { success = false, error = $"GameObject not found in scene: {objectName}" };
}
// Ensure target directory exists
string targetDir = Path.GetDirectoryName(prefabPath);
if (!Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
GameObject prefab = PrefabUtility.SaveAsPrefabAsset(sourceObject, prefabPath);
if (prefab == null)
{
return new { success = false, error = "Failed to create prefab. Verify the path is writable." };
}
return new
{
success = true,
message = $"Successfully created prefab at {prefabPath}",
path = prefabPath
};
}
catch (System.Exception e)
{
return new {
success = false,
error = $"Failed to create prefab: {e.Message}",
stackTrace = e.StackTrace,
sourceInfo = $"Object: {@params["object_name"]}, Path: {@params["prefab_path"]}"
};
}
}
/// <summary>
/// Applies changes from a prefab instance back to the original prefab asset
/// </summary>
public static object ApplyPrefab(JObject @params)
{
string objectName = (string)@params["object_name"];
GameObject instance = GameObject.Find(objectName);
if (instance == null)
{
return new { error = $"GameObject not found in scene: {objectName}" };
}
Object prefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(instance);
if (prefabAsset == null)
{
return new { error = "Selected object is not a prefab instance" };
}
PrefabUtility.ApplyPrefabInstance(instance, InteractionMode.AutomatedAction);
return new { message = "Successfully applied changes to prefab asset" };
}
/// <summary>
/// Gets a list of assets in the project, optionally filtered by type
/// </summary>
public static object GetAssetList(JObject @params)
{
string type = (string)@params["type"];
string searchPattern = (string)@params["search_pattern"] ?? "*";
string folder = (string)@params["folder"] ?? "Assets";
var guids = AssetDatabase.FindAssets(searchPattern, new[] { folder });
var assets = new List<object>();
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var assetType = AssetDatabase.GetMainAssetTypeAtPath(path);
// Skip if type filter is specified and doesn't match
if (!string.IsNullOrEmpty(type) && assetType?.Name != type)
continue;
assets.Add(new
{
name = Path.GetFileNameWithoutExtension(path),
path = path,
type = assetType?.Name ?? "Unknown",
guid = guid
});
}
return new { assets };
}
}
}

View File

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

View File

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace MCPServer.Editor.Commands
{
/// <summary>
/// Registry for all MCP command handlers
/// </summary>
public static class CommandRegistry
{
private static readonly Dictionary<string, Func<JObject, object>> _handlers = new()
{
// Scene management commands
{ "GET_SCENE_INFO", _ => SceneCommandHandler.GetSceneInfo() },
{ "OPEN_SCENE", parameters => SceneCommandHandler.OpenScene(parameters) },
{ "SAVE_SCENE", _ => SceneCommandHandler.SaveScene() },
{ "NEW_SCENE", parameters => SceneCommandHandler.NewScene(parameters) },
{ "CHANGE_SCENE", parameters => SceneCommandHandler.ChangeScene(parameters) },
// Asset management commands
{ "IMPORT_ASSET", parameters => AssetCommandHandler.ImportAsset(parameters) },
{ "INSTANTIATE_PREFAB", parameters => AssetCommandHandler.InstantiatePrefab(parameters) },
{ "CREATE_PREFAB", parameters => AssetCommandHandler.CreatePrefab(parameters) },
{ "APPLY_PREFAB", parameters => AssetCommandHandler.ApplyPrefab(parameters) },
{ "GET_ASSET_LIST", parameters => AssetCommandHandler.GetAssetList(parameters) },
// Object management commands
{ "GET_OBJECT_PROPERTIES", parameters => ObjectCommandHandler.GetObjectProperties(parameters) },
{ "GET_COMPONENT_PROPERTIES", parameters => ObjectCommandHandler.GetComponentProperties(parameters) },
{ "FIND_OBJECTS_BY_NAME", parameters => ObjectCommandHandler.FindObjectsByName(parameters) },
{ "FIND_OBJECTS_BY_TAG", parameters => ObjectCommandHandler.FindObjectsByTag(parameters) },
{ "GET_HIERARCHY", _ => ObjectCommandHandler.GetHierarchy() },
{ "SELECT_OBJECT", parameters => ObjectCommandHandler.SelectObject(parameters) },
{ "GET_SELECTED_OBJECT", _ => ObjectCommandHandler.GetSelectedObject() },
// Editor control commands
{ "EDITOR_CONTROL", parameters => EditorControlHandler.HandleEditorControl(parameters) }
};
/// <summary>
/// Gets a command handler by name
/// </summary>
/// <param name="commandName">Name of the command to get</param>
/// <returns>The command handler function if found, null otherwise</returns>
public static Func<JObject, object> GetHandler(string commandName)
{
return _handlers.TryGetValue(commandName, out var handler) ? handler : null;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 278afefc8a504e742b68893419a0ec40

View File

@ -0,0 +1,155 @@
using UnityEngine;
using UnityEditor;
using UnityEditor.Build.Reporting;
using Newtonsoft.Json.Linq;
/// <summary>
/// Handles editor control commands like undo, redo, play, pause, stop, and build operations.
/// </summary>
public static class EditorControlHandler
{
/// <summary>
/// Handles editor control commands
/// </summary>
public static object HandleEditorControl(JObject @params)
{
string command = (string)@params["command"];
JObject commandParams = (JObject)@params["params"];
switch (command.ToUpper())
{
case "UNDO":
return HandleUndo();
case "REDO":
return HandleRedo();
case "PLAY":
return HandlePlay();
case "PAUSE":
return HandlePause();
case "STOP":
return HandleStop();
case "BUILD":
return HandleBuild(commandParams);
case "EXECUTE_COMMAND":
return HandleExecuteCommand(commandParams);
default:
return new { error = $"Unknown editor control command: {command}" };
}
}
private static object HandleUndo()
{
Undo.PerformUndo();
return new { message = "Undo performed successfully" };
}
private static object HandleRedo()
{
Undo.PerformRedo();
return new { message = "Redo performed successfully" };
}
private static object HandlePlay()
{
if (!EditorApplication.isPlaying)
{
EditorApplication.isPlaying = true;
return new { message = "Entered play mode" };
}
return new { message = "Already in play mode" };
}
private static object HandlePause()
{
if (EditorApplication.isPlaying)
{
EditorApplication.isPaused = !EditorApplication.isPaused;
return new { message = EditorApplication.isPaused ? "Game paused" : "Game resumed" };
}
return new { message = "Not in play mode" };
}
private static object HandleStop()
{
if (EditorApplication.isPlaying)
{
EditorApplication.isPlaying = false;
return new { message = "Exited play mode" };
}
return new { message = "Not in play mode" };
}
private static object HandleBuild(JObject @params)
{
string platform = (string)@params["platform"];
string buildPath = (string)@params["buildPath"];
try
{
BuildTarget target = GetBuildTarget(platform);
if ((int)target == -1)
{
return new { error = $"Unsupported platform: {platform}" };
}
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
buildPlayerOptions.scenes = GetEnabledScenes();
buildPlayerOptions.target = target;
buildPlayerOptions.locationPathName = buildPath;
BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);
return new
{
message = "Build completed successfully",
summary = report.summary
};
}
catch (System.Exception e)
{
return new { error = $"Build failed: {e.Message}" };
}
}
private static object HandleExecuteCommand(JObject @params)
{
string commandName = (string)@params["commandName"];
try
{
EditorApplication.ExecuteMenuItem(commandName);
return new { message = $"Executed command: {commandName}" };
}
catch (System.Exception e)
{
return new { error = $"Failed to execute command: {e.Message}" };
}
}
private static BuildTarget GetBuildTarget(string platform)
{
BuildTarget target;
switch (platform.ToLower())
{
case "windows": target = BuildTarget.StandaloneWindows64; break;
case "mac": target = BuildTarget.StandaloneOSX; break;
case "linux": target = BuildTarget.StandaloneLinux64; break;
case "android": target = BuildTarget.Android; break;
case "ios": target = BuildTarget.iOS; break;
case "webgl": target = BuildTarget.WebGL; break;
default: target = (BuildTarget)(-1); break; // Invalid target
}
return target;
}
private static string[] GetEnabledScenes()
{
var scenes = new System.Collections.Generic.List<string>();
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
{
if (EditorBuildSettings.scenes[i].enabled)
{
scenes.Add(EditorBuildSettings.scenes[i].path);
}
}
return scenes.ToArray();
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4c3f2560f6bf61f4c8f33e250f381a17

View File

@ -0,0 +1,47 @@
using UnityEngine;
using Newtonsoft.Json.Linq;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering;
namespace MCPServer.Editor.Commands
{
/// <summary>
/// Handles material-related commands
/// </summary>
public static class MaterialCommandHandler
{
/// <summary>
/// Sets or modifies a material on an object
/// </summary>
public static object SetMaterial(JObject @params)
{
string objectName = (string)@params["object_name"] ?? throw new System.Exception("Parameter 'object_name' is required.");
var obj = GameObject.Find(objectName) ?? throw new System.Exception($"Object '{objectName}' not found.");
var renderer = obj.GetComponent<Renderer>() ?? throw new System.Exception($"Object '{objectName}' has no renderer.");
// Check if URP is being used
bool isURP = GraphicsSettings.currentRenderPipeline is UniversalRenderPipelineAsset;
// Create material with appropriate shader based on render pipeline
Material material;
if (isURP)
{
material = new Material(Shader.Find("Universal Render Pipeline/Lit"));
}
else
{
material = new Material(Shader.Find("Standard"));
}
if (@params.ContainsKey("material_name")) material.name = (string)@params["material_name"];
if (@params.ContainsKey("color"))
{
var colorArray = (JArray)@params["color"] ?? throw new System.Exception("Invalid color parameter.");
if (colorArray.Count != 3) throw new System.Exception("Color must be an array of 3 floats [r, g, b].");
material.color = new Color((float)colorArray[0], (float)colorArray[1], (float)colorArray[2]);
}
renderer.material = material;
return new { material_name = material.name };
}
}
}

View File

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

View File

@ -0,0 +1,387 @@
using UnityEngine;
using Newtonsoft.Json.Linq;
using MCPServer.Editor.Helpers;
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine.SceneManagement;
namespace MCPServer.Editor.Commands
{
/// <summary>
/// Handles object-related commands
/// </summary>
public static class ObjectCommandHandler
{
/// <summary>
/// Gets information about a specific object
/// </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.");
return new
{
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 }
};
}
/// <summary>
/// Creates a new object in the scene
/// </summary>
public static object CreateObject(JObject @params)
{
string type = (string)@params["type"] ?? throw new System.Exception("Parameter 'type' is required.");
GameObject obj = type.ToUpper() switch
{
"CUBE" => GameObject.CreatePrimitive(PrimitiveType.Cube),
"SPHERE" => GameObject.CreatePrimitive(PrimitiveType.Sphere),
"CYLINDER" => GameObject.CreatePrimitive(PrimitiveType.Cylinder),
"CAPSULE" => GameObject.CreatePrimitive(PrimitiveType.Capsule),
"PLANE" => GameObject.CreatePrimitive(PrimitiveType.Plane),
"EMPTY" => new GameObject(),
"CAMERA" => new GameObject("Camera") { }.AddComponent<Camera>().gameObject,
"LIGHT" => new GameObject("Light") { }.AddComponent<Light>().gameObject,
_ => throw new System.Exception($"Unsupported object type: {type}")
};
if (@params.ContainsKey("name")) obj.name = (string)@params["name"];
if (@params.ContainsKey("location")) obj.transform.position = Vector3Helper.ParseVector3((JArray)@params["location"]);
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 };
}
/// <summary>
/// Modifies an existing object's properties
/// </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.");
// Handle basic transform properties
if (@params.ContainsKey("location")) obj.transform.position = Vector3Helper.ParseVector3((JArray)@params["location"]);
if (@params.ContainsKey("rotation")) obj.transform.eulerAngles = Vector3Helper.ParseVector3((JArray)@params["rotation"]);
if (@params.ContainsKey("scale")) obj.transform.localScale = Vector3Helper.ParseVector3((JArray)@params["scale"]);
if (@params.ContainsKey("visible")) obj.SetActive((bool)@params["visible"]);
// Handle parent setting
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.");
obj.transform.SetParent(parent.transform);
}
// Handle component operations
if (@params.ContainsKey("add_component"))
{
string componentType = (string)@params["add_component"];
Type type = componentType switch
{
"Rigidbody" => typeof(Rigidbody),
"BoxCollider" => typeof(BoxCollider),
"SphereCollider" => typeof(SphereCollider),
"CapsuleCollider" => typeof(CapsuleCollider),
"MeshCollider" => typeof(MeshCollider),
"Camera" => typeof(Camera),
"Light" => typeof(Light),
"Renderer" => typeof(Renderer),
"MeshRenderer" => typeof(MeshRenderer),
"SkinnedMeshRenderer" => typeof(SkinnedMeshRenderer),
"Animator" => typeof(Animator),
"AudioSource" => typeof(AudioSource),
"AudioListener" => typeof(AudioListener),
"ParticleSystem" => typeof(ParticleSystem),
"ParticleSystemRenderer" => typeof(ParticleSystemRenderer),
"TrailRenderer" => typeof(TrailRenderer),
"LineRenderer" => typeof(LineRenderer),
"TextMesh" => typeof(TextMesh),
"TextMeshPro" => typeof(TMPro.TextMeshPro),
"TextMeshProUGUI" => typeof(TMPro.TextMeshProUGUI),
_ => Type.GetType($"UnityEngine.{componentType}") ??
Type.GetType(componentType) ??
throw new System.Exception($"Component type '{componentType}' not found.")
};
obj.AddComponent(type);
}
if (@params.ContainsKey("remove_component"))
{
string componentType = (string)@params["remove_component"];
Type type = Type.GetType($"UnityEngine.{componentType}") ??
Type.GetType(componentType) ??
throw new System.Exception($"Component type '{componentType}' not found.");
var component = obj.GetComponent(type);
if (component != null)
UnityEngine.Object.DestroyImmediate(component);
}
// Handle property setting
if (@params.ContainsKey("set_property"))
{
var propertyData = (JObject)@params["set_property"];
string componentType = (string)propertyData["component"];
string propertyName = (string)propertyData["property"];
var value = propertyData["value"];
// Handle GameObject properties separately
if (componentType == "GameObject")
{
var gameObjectProperty = typeof(GameObject).GetProperty(propertyName) ??
throw new System.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 };
}
// Handle component properties
Type type = componentType switch
{
"Rigidbody" => typeof(Rigidbody),
"BoxCollider" => typeof(BoxCollider),
"SphereCollider" => typeof(SphereCollider),
"CapsuleCollider" => typeof(CapsuleCollider),
"MeshCollider" => typeof(MeshCollider),
"Camera" => typeof(Camera),
"Light" => typeof(Light),
"Renderer" => typeof(Renderer),
"MeshRenderer" => typeof(MeshRenderer),
"SkinnedMeshRenderer" => typeof(SkinnedMeshRenderer),
"Animator" => typeof(Animator),
"AudioSource" => typeof(AudioSource),
"AudioListener" => typeof(AudioListener),
"ParticleSystem" => typeof(ParticleSystem),
"ParticleSystemRenderer" => typeof(ParticleSystemRenderer),
"TrailRenderer" => typeof(TrailRenderer),
"LineRenderer" => typeof(LineRenderer),
"TextMesh" => typeof(TextMesh),
"TextMeshPro" => typeof(TMPro.TextMeshPro),
"TextMeshProUGUI" => typeof(TMPro.TextMeshProUGUI),
_ => Type.GetType($"UnityEngine.{componentType}") ??
Type.GetType(componentType) ??
throw new System.Exception($"Component type '{componentType}' not found.")
};
var component = obj.GetComponent(type) ??
throw new System.Exception($"Component '{componentType}' not found on object '{name}'.");
var property = type.GetProperty(propertyName) ??
throw new System.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 };
}
/// <summary>
/// Deletes an object from the scene
/// </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.");
UnityEngine.Object.DestroyImmediate(obj);
return new { name };
}
/// <summary>
/// Gets all properties of a specified game object
/// </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.");
var components = obj.GetComponents<Component>()
.Select(c => new
{
type = c.GetType().Name,
properties = GetComponentProperties(c)
})
.ToList();
return new
{
name = obj.name,
tag = obj.tag,
layer = obj.layer,
active = obj.activeSelf,
transform = new
{
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 }
},
components
};
}
/// <summary>
/// Gets properties of a specific component
/// </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.");
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}'.");
return GetComponentProperties(component);
}
/// <summary>
/// Finds objects by name in the scene
/// </summary>
public static object FindObjectsByName(JObject @params)
{
string name = (string)@params["name"] ?? throw new System.Exception("Parameter 'name' is required.");
var objects = GameObject.FindObjectsOfType<GameObject>()
.Where(o => o.name.Contains(name))
.Select(o => new
{
name = o.name,
path = GetGameObjectPath(o)
})
.ToList();
return new { objects };
}
/// <summary>
/// Finds objects by tag in the scene
/// </summary>
public static object FindObjectsByTag(JObject @params)
{
string tag = (string)@params["tag"] ?? throw new System.Exception("Parameter 'tag' is required.");
var objects = GameObject.FindGameObjectsWithTag(tag)
.Select(o => new
{
name = o.name,
path = GetGameObjectPath(o)
})
.ToList();
return new { objects };
}
/// <summary>
/// Gets the current hierarchy of game objects in the scene
/// </summary>
public static object GetHierarchy()
{
var rootObjects = SceneManager.GetActiveScene().GetRootGameObjects();
var hierarchy = rootObjects.Select(o => BuildHierarchyNode(o)).ToList();
return new { hierarchy };
}
/// <summary>
/// Selects a specified game object in the editor
/// </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.");
Selection.activeGameObject = obj;
return new { name = obj.name };
}
/// <summary>
/// Gets the currently selected game object in the editor
/// </summary>
public static object GetSelectedObject()
{
var selected = Selection.activeGameObject;
if (selected == null)
return new { selected = (object)null };
return new
{
selected = new
{
name = selected.name,
path = GetGameObjectPath(selected)
}
};
}
// Helper methods
private static Dictionary<string, object> GetComponentProperties(Component component)
{
var properties = new Dictionary<string, object>();
var serializedObject = new SerializedObject(component);
var property = serializedObject.GetIterator();
while (property.Next(true))
{
properties[property.name] = GetPropertyValue(property);
}
return properties;
}
private static object GetPropertyValue(SerializedProperty property)
{
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
return property.intValue;
case SerializedPropertyType.Float:
return property.floatValue;
case SerializedPropertyType.Boolean:
return property.boolValue;
case SerializedPropertyType.String:
return property.stringValue;
case SerializedPropertyType.Vector3:
return new[] { property.vector3Value.x, property.vector3Value.y, property.vector3Value.z };
case SerializedPropertyType.Vector2:
return new[] { property.vector2Value.x, property.vector2Value.y };
case SerializedPropertyType.Color:
return new[] { property.colorValue.r, property.colorValue.g, property.colorValue.b, property.colorValue.a };
case SerializedPropertyType.ObjectReference:
return property.objectReferenceValue ? property.objectReferenceValue.name : null;
default:
return property.propertyType.ToString();
}
}
private static string GetGameObjectPath(GameObject obj)
{
var path = obj.name;
var parent = obj.transform.parent;
while (parent != null)
{
path = parent.name + "/" + path;
parent = parent.parent;
}
return path;
}
private static object BuildHierarchyNode(GameObject obj)
{
return new
{
name = obj.name,
children = Enumerable.Range(0, obj.transform.childCount)
.Select(i => BuildHierarchyNode(obj.transform.GetChild(i).gameObject))
.ToList()
};
}
}
}

View File

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

View File

@ -0,0 +1,140 @@
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Linq;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
namespace MCPServer.Editor.Commands
{
/// <summary>
/// Handles scene-related commands for the MCP Server
/// </summary>
public static class SceneCommandHandler
{
/// <summary>
/// Gets information about the current scene
/// </summary>
/// <returns>Scene information including name and root objects</returns>
public static object GetSceneInfo()
{
var scene = SceneManager.GetActiveScene();
var rootObjects = scene.GetRootGameObjects().Select(o => o.name).ToArray();
return new { sceneName = scene.name, rootObjects };
}
/// <summary>
/// Opens a specified scene in the Unity editor
/// </summary>
/// <param name="params">Parameters containing the scene path</param>
/// <returns>Result of the operation</returns>
public static object OpenScene(JObject @params)
{
try
{
string scenePath = (string)@params["scene_path"];
if (string.IsNullOrEmpty(scenePath))
return new { success = false, error = "Scene path cannot be empty" };
if (!System.IO.File.Exists(scenePath))
return new { success = false, error = $"Scene file not found: {scenePath}" };
EditorSceneManager.OpenScene(scenePath);
return new { success = true, message = $"Opened scene: {scenePath}" };
}
catch (System.Exception e)
{
return new { success = false, error = $"Failed to open scene: {e.Message}", stackTrace = e.StackTrace };
}
}
/// <summary>
/// Saves the current scene
/// </summary>
/// <returns>Result of the operation</returns>
public static object SaveScene()
{
try
{
var scene = SceneManager.GetActiveScene();
EditorSceneManager.SaveScene(scene);
return new { success = true, message = $"Saved scene: {scene.path}" };
}
catch (System.Exception e)
{
return new { success = false, error = $"Failed to save scene: {e.Message}", stackTrace = e.StackTrace };
}
}
/// <summary>
/// Creates a new empty scene
/// </summary>
/// <param name="params">Parameters containing the new scene path</param>
/// <returns>Result of the operation</returns>
public static object NewScene(JObject @params)
{
try
{
string scenePath = (string)@params["scene_path"];
if (string.IsNullOrEmpty(scenePath))
return new { success = false, error = "Scene path cannot be empty" };
// Create new scene
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);
// Ensure the scene is loaded and active
if (!scene.isLoaded)
{
EditorSceneManager.LoadScene(scenePath);
}
// Save the scene
EditorSceneManager.SaveScene(scene, scenePath);
// Force a refresh of the scene view
EditorApplication.ExecuteMenuItem("Window/General/Scene");
return new { success = true, message = $"Created new scene at: {scenePath}" };
}
catch (System.Exception e)
{
return new { success = false, error = $"Failed to create new scene: {e.Message}", stackTrace = e.StackTrace };
}
}
/// <summary>
/// Changes to a different scene, optionally saving the current one
/// </summary>
/// <param name="params">Parameters containing the target scene path and save option</param>
/// <returns>Result of the operation</returns>
public static object ChangeScene(JObject @params)
{
try
{
string scenePath = (string)@params["scene_path"];
bool saveCurrent = @params["save_current"]?.Value<bool>() ?? false;
if (string.IsNullOrEmpty(scenePath))
return new { success = false, error = "Scene path cannot be empty" };
if (!System.IO.File.Exists(scenePath))
return new { success = false, error = $"Scene file not found: {scenePath}" };
// Save current scene if requested
if (saveCurrent)
{
var currentScene = SceneManager.GetActiveScene();
EditorSceneManager.SaveScene(currentScene);
}
// Open the new scene
EditorSceneManager.OpenScene(scenePath);
return new { success = true, message = $"Changed to scene: {scenePath}" };
}
catch (System.Exception e)
{
return new { success = false, error = $"Failed to change scene: {e.Message}", stackTrace = e.StackTrace };
}
}
}
}

View File

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

View File

@ -0,0 +1,212 @@
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Text;
using System.Linq;
using Newtonsoft.Json.Linq;
using MCPServer.Editor.Helpers;
namespace MCPServer.Editor.Commands
{
/// <summary>
/// Handles script-related commands for Unity
/// </summary>
public static class ScriptCommandHandler
{
/// <summary>
/// Views the contents of a Unity script file
/// </summary>
public static object ViewScript(JObject @params)
{
string scriptPath = (string)@params["script_path"] ?? throw new System.Exception("Parameter 'script_path' is required.");
string fullPath = Path.Combine(Application.dataPath, scriptPath);
if (!File.Exists(fullPath))
throw new System.Exception($"Script file not found: {scriptPath}");
return new { content = File.ReadAllText(fullPath) };
}
/// <summary>
/// Ensures the Scripts folder exists in the project
/// </summary>
private static void EnsureScriptsFolderExists()
{
string scriptsFolderPath = Path.Combine(Application.dataPath, "Scripts");
if (!Directory.Exists(scriptsFolderPath))
{
Directory.CreateDirectory(scriptsFolderPath);
AssetDatabase.Refresh();
}
}
/// <summary>
/// Creates a new Unity script file in the Scripts folder
/// </summary>
public static object CreateScript(JObject @params)
{
string scriptName = (string)@params["script_name"] ?? throw new System.Exception("Parameter 'script_name' is required.");
string scriptType = (string)@params["script_type"] ?? "MonoBehaviour";
string namespaceName = (string)@params["namespace"];
string template = (string)@params["template"];
// Ensure script name ends with .cs
if (!scriptName.EndsWith(".cs"))
scriptName += ".cs";
// Ensure Scripts folder exists
EnsureScriptsFolderExists();
// Create namespace-based folder structure if namespace is specified
string scriptPath = "Scripts";
if (!string.IsNullOrEmpty(namespaceName))
{
scriptPath = Path.Combine(scriptPath, namespaceName.Replace('.', '/'));
string namespaceFolderPath = Path.Combine(Application.dataPath, scriptPath);
if (!Directory.Exists(namespaceFolderPath))
{
Directory.CreateDirectory(namespaceFolderPath);
AssetDatabase.Refresh();
}
}
// Create the script content
StringBuilder content = new StringBuilder();
// Add namespace if specified
if (!string.IsNullOrEmpty(namespaceName))
{
content.AppendLine($"namespace {namespaceName}");
content.AppendLine("{");
}
// Add class definition
content.AppendLine($" public class {Path.GetFileNameWithoutExtension(scriptName)} : {scriptType}");
content.AppendLine(" {");
// Add default Unity methods based on script type
if (scriptType == "MonoBehaviour")
{
content.AppendLine(" private void Start()");
content.AppendLine(" {");
content.AppendLine(" // Initialize your component here");
content.AppendLine(" }");
content.AppendLine();
content.AppendLine(" private void Update()");
content.AppendLine(" {");
content.AppendLine(" // Update your component here");
content.AppendLine(" }");
}
else if (scriptType == "ScriptableObject")
{
content.AppendLine(" private void OnEnable()");
content.AppendLine(" {");
content.AppendLine(" // Initialize your ScriptableObject here");
content.AppendLine(" }");
}
// Close class
content.AppendLine(" }");
// Close namespace if specified
if (!string.IsNullOrEmpty(namespaceName))
{
content.AppendLine("}");
}
// Create the script file in the Scripts folder
string fullPath = Path.Combine(Application.dataPath, scriptPath, scriptName);
File.WriteAllText(fullPath, content.ToString());
// Refresh the AssetDatabase
AssetDatabase.Refresh();
return new { message = $"Created script: {Path.Combine(scriptPath, scriptName)}" };
}
/// <summary>
/// Updates the contents of an existing Unity script
/// </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 fullPath = Path.Combine(Application.dataPath, scriptPath);
if (!File.Exists(fullPath))
throw new System.Exception($"Script file not found: {scriptPath}");
// Write new content
File.WriteAllText(fullPath, content);
// Refresh the AssetDatabase
AssetDatabase.Refresh();
return new { message = $"Updated script: {scriptPath}" };
}
/// <summary>
/// Lists all script files in a specified folder
/// </summary>
public static object ListScripts(JObject @params)
{
string folderPath = (string)@params["folder_path"] ?? "Assets";
string fullPath = Path.Combine(Application.dataPath, folderPath);
if (!Directory.Exists(fullPath))
throw new System.Exception($"Folder not found: {folderPath}");
string[] scripts = Directory.GetFiles(fullPath, "*.cs", SearchOption.AllDirectories)
.Select(path => path.Replace(Application.dataPath, "Assets"))
.ToArray();
return new { scripts };
}
/// <summary>
/// Attaches a script component to a GameObject
/// </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.");
// Find the target object
GameObject targetObject = GameObject.Find(objectName);
if (targetObject == null)
throw new System.Exception($"Object '{objectName}' not found in scene.");
// Ensure script name ends with .cs
if (!scriptName.EndsWith(".cs"))
scriptName += ".cs";
// Find the script asset
string[] guids = AssetDatabase.FindAssets(Path.GetFileNameWithoutExtension(scriptName));
if (guids.Length == 0)
throw new System.Exception($"Script '{scriptName}' not found in project.");
// Get the script asset
string scriptPath = AssetDatabase.GUIDToAssetPath(guids[0]);
MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptPath);
if (scriptAsset == null)
throw new System.Exception($"Failed to load script asset: {scriptName}");
// Get the script type
System.Type scriptType = scriptAsset.GetClass();
if (scriptType == null)
throw new System.Exception($"Script '{scriptName}' does not contain a valid MonoBehaviour class.");
// Add the component
Component component = targetObject.AddComponent(scriptType);
if (component == null)
throw new System.Exception($"Failed to add component of type {scriptType.Name} to object '{objectName}'.");
return new
{
message = $"Successfully attached script '{scriptName}' to object '{objectName}'",
component_type = scriptType.Name
};
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 92d45020dab10bc4f9466aac6f8f3a71

8
Editor/Helpers.meta Normal file
View File

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

View File

@ -0,0 +1,24 @@
using UnityEngine;
using Newtonsoft.Json.Linq;
namespace MCPServer.Editor.Helpers
{
/// <summary>
/// Helper class for Vector3 operations
/// </summary>
public static class Vector3Helper
{
/// <summary>
/// Parses a JArray into a Vector3
/// </summary>
/// <param name="array">The array containing x, y, z coordinates</param>
/// <returns>A Vector3 with the parsed coordinates</returns>
/// <exception cref="System.Exception">Thrown when array is invalid</exception>
public static Vector3 ParseVector3(JArray array)
{
if (array == null || array.Count != 3)
throw new System.Exception("Vector3 must be an array of 3 floats [x, y, z].");
return new Vector3((float)array[0], (float)array[1], (float)array[2]);
}
}
}

View File

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

354
Editor/MCPEditorWindow.cs Normal file
View File

@ -0,0 +1,354 @@
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;
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;
[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 bool isPythonServerConnected = false;
private string pythonServerStatus = "Not Connected";
private Color pythonServerStatusColor = Color.red;
private ServerConfig serverConfig;
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()
{
// Load server configuration
LoadServerConfig();
// Check initial states
isUnityBridgeRunning = UnityMCPBridge.IsRunning;
CheckPythonServerConnection();
}
private void LoadServerConfig()
{
try
{
string configPath = Path.Combine(Application.dataPath, "MCPServer", "config.json");
if (File.Exists(configPath))
{
string jsonConfig = File.ReadAllText(configPath);
serverConfig = JsonConvert.DeserializeObject<ServerConfig>(jsonConfig);
UnityEngine.Debug.Log($"Loaded server config: Unity Port = {serverConfig.unityPort}, MCP Port = {serverConfig.mcpPort}");
}
else
{
UnityEngine.Debug.LogError("Server config file not found!");
serverConfig = new DefaultServerConfig();
}
}
catch (Exception e)
{
UnityEngine.Debug.LogError($"Error loading server config: {e.Message}");
serverConfig = new DefaultServerConfig();
}
}
private void Update()
{
// Check Python server connection periodically
if (Time.realtimeSinceStartup - lastCheckTime >= CONNECTION_CHECK_INTERVAL)
{
CheckPythonServerConnection();
lastCheckTime = Time.realtimeSinceStartup;
}
}
private async void CheckPythonServerConnection()
{
if (serverConfig == null)
{
LoadServerConfig(); // Reload config if not loaded
}
try
{
using (var client = new TcpClient())
{
// Try to connect with a short timeout
var connectTask = client.ConnectAsync(serverConfig.unityHost, serverConfig.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
isPythonServerConnected = true;
pythonServerStatus = "Connected";
pythonServerStatusColor = Color.green;
UnityEngine.Debug.Log($"Python server connected successfully on port {serverConfig.unityPort}");
}
else
{
// No response received
isPythonServerConnected = false;
pythonServerStatus = "No Response";
pythonServerStatusColor = Color.yellow;
UnityEngine.Debug.LogWarning($"Python server not responding on port {serverConfig.unityPort}");
}
}
catch (Exception e)
{
// Connection established but communication failed
isPythonServerConnected = false;
pythonServerStatus = "Communication Error";
pythonServerStatusColor = Color.yellow;
UnityEngine.Debug.LogWarning($"Error communicating with Python server: {e.Message}");
}
}
else
{
// Connection failed
isPythonServerConnected = false;
pythonServerStatus = "Not Connected";
pythonServerStatusColor = Color.red;
UnityEngine.Debug.LogWarning($"Python server is not running or not accessible on port {serverConfig.unityPort}");
}
client.Close();
}
}
catch (Exception e)
{
isPythonServerConnected = false;
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: {serverConfig?.unityPort}");
EditorGUILayout.LabelField($"MCP Port: {serverConfig?.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: {serverConfig?.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));
// Get the absolute path to the Python directory
string pythonDir = Path.GetFullPath(Path.Combine(Application.dataPath, "MCPServer", "Python"));
UnityEngine.Debug.Log($"Python directory path: {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)
{
claudeConfigStatus = "Configuration failed";
UnityEngine.Debug.LogError($"Failed to configure Claude Desktop: {e.Message}");
}
}
}

View File

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

8
Editor/Models.meta Normal file
View File

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

20
Editor/Models/Command.cs Normal file
View File

@ -0,0 +1,20 @@
using Newtonsoft.Json.Linq;
namespace MCPServer.Editor.Models
{
/// <summary>
/// Represents a command received from the MCP client
/// </summary>
public class Command
{
/// <summary>
/// The type of command to execute
/// </summary>
public string type { get; set; }
/// <summary>
/// The parameters for the command
/// </summary>
public JObject @params { get; set; }
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6c357a62c6b79bb45ba78f1f92f0502a

351
Editor/UnityMCPBridge.cs Normal file
View File

@ -0,0 +1,351 @@
using System;
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;
[InitializeOnLoad]
public static partial class UnityMCPBridge
{
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 ServerConfig serverConfig;
// Add public property to expose running state
public static bool IsRunning => isRunning;
static UnityMCPBridge()
{
LoadServerConfig();
Start();
EditorApplication.quitting += Stop;
}
private static void LoadServerConfig()
{
try
{
string configPath = Path.Combine(Application.dataPath, "MCPServer", "config.json");
if (File.Exists(configPath))
{
string jsonConfig = File.ReadAllText(configPath);
serverConfig = JsonConvert.DeserializeObject<ServerConfig>(jsonConfig);
Debug.Log($"Loaded server config: Unity Port = {serverConfig.unityPort}, MCP Port = {serverConfig.mcpPort}");
}
else
{
Debug.LogError("Server config file not found!");
serverConfig = new DefaultServerConfig();
}
}
catch (Exception e)
{
Debug.LogError($"Error loading server config: {e.Message}");
serverConfig = new DefaultServerConfig();
}
}
public static void Start()
{
if (isRunning) return;
isRunning = true;
listener = new TcpListener(IPAddress.Loopback, serverConfig.unityPort);
listener.Start();
Debug.Log($"UnityMCPBridge started on port {serverConfig.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)
{
try
{
var client = await listener.AcceptTcpClientAsync();
_ = HandleClientAsync(client); // Fire and forget each client connection
}
catch (Exception ex)
{
if (isRunning) Debug.LogError($"Listener error: {ex.Message}");
}
}
}
private static async Task HandleClientAsync(TcpClient client)
{
using (client)
using (var stream = client.GetStream())
{
var buffer = new byte[8192];
while (isRunning)
{
try
{
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 = ex.Message,
commandType = "Unknown (error during processing)",
receivedText = commandText?.Length > 50 ? commandText.Substring(0, 50) + "..." : commandText
};
string responseJson = JsonConvert.SerializeObject(response);
tcs.SetResult(responseJson);
}
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))
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;
}
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";
}
}
}

View File

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

210
HOW_TO_ADD_A_TOOL.md Normal file
View File

@ -0,0 +1,210 @@
# Unity MCP Server
This directory contains the Unity MCP Server implementation, which provides a bridge between Python and Unity Editor functionality.
## Adding New Tools
To add a new tool to the MCP Server, follow these steps:
### 1. Create the C# Command Handler
First, create or modify a command handler in the `Editor/Commands` directory:
```csharp
// Example: NewCommandHandler.cs
public static class NewCommandHandler
{
public static object HandleNewCommand(JObject @params)
{
// Extract parameters
string param1 = (string)@params["param1"];
int param2 = (int)@params["param2"];
// Implement the Unity-side functionality
// ...
// Return results
return new {
message = "Operation successful",
result = someResult
};
}
}
```
### 2. Register the Command Handler
Add your command handler to the `CommandRegistry.cs` in the `Editor/Commands` directory:
```csharp
public static class CommandRegistry
{
private static readonly Dictionary<string, Func<JObject, object>> _handlers = new()
{
// ... existing handlers ...
{ "NEW_COMMAND", NewCommandHandler.HandleNewCommand }
};
}
```
### 3. Create the Python Tool
Add your tool to the appropriate Python module in the `Python/tools` directory:
```python
@mcp.tool()
def new_tool(
ctx: Context,
param1: str,
param2: int
) -> str:
"""Description of what the tool does.
Args:
ctx: The MCP context
param1: Description of param1
param2: Description of param2
Returns:
str: Success message or error details
"""
try:
response = get_unity_connection().send_command("NEW_COMMAND", {
"param1": param1,
"param2": param2
})
return response.get("message", "Operation successful")
except Exception as e:
return f"Error executing operation: {str(e)}"
```
### 4. Register the Tool
Ensure your tool is registered in the appropriate registration function:
```python
# In Python/tools/__init__.py
def register_all_tools(mcp):
register_scene_tools(mcp)
register_script_tools(mcp)
register_material_tools(mcp)
# Add your new tool registration if needed
```
### 5. Update the Prompt
If your tool should be exposed to users, update the prompt in `Python/server.py`:
```python
@mcp.prompt()
def asset_creation_strategy() -> str:
return (
"Follow these Unity best practices:\n\n"
"1. **Your Category**:\n"
" - Use `new_tool(param1, param2)` to do something\n"
# ... rest of the prompt ...
)
```
## Best Practices
1. **Error Handling**:
- Always include try-catch blocks in Python tools
- Validate parameters in C# handlers
- Return meaningful error messages
2. **Documentation**:
- Add XML documentation to C# handlers
- Include detailed docstrings in Python tools
- Update the prompt with clear usage instructions
3. **Parameter Validation**:
- Validate parameters on both Python and C# sides
- Use appropriate types (str, int, float, List, etc.)
- Provide default values when appropriate
4. **Testing**:
- Test the tool in both Unity Editor and Python environments
- Verify error handling works as expected
- Check that the tool integrates well with existing functionality
5. **Code Organization**:
- Group related tools in appropriate handler classes
- Keep tools focused and single-purpose
- Follow existing naming conventions
## Example Implementation
Here's a complete example of adding a new tool:
1. **C# Handler** (`Editor/Commands/ExampleHandler.cs`):
```csharp
public static class ExampleHandler
{
public static object CreatePrefab(JObject @params)
{
string prefabName = (string)@params["prefab_name"];
string template = (string)@params["template"];
// Implementation
GameObject prefab = new GameObject(prefabName);
// ... setup prefab ...
return new {
message = $"Created prefab: {prefabName}",
path = $"Assets/Prefabs/{prefabName}.prefab"
};
}
}
```
2. **Python Tool** (`Python/tools/example_tools.py`):
```python
@mcp.tool()
def create_prefab(
ctx: Context,
prefab_name: str,
template: str = "default"
) -> str:
"""Create a new prefab in the project.
Args:
ctx: The MCP context
prefab_name: Name for the new prefab
template: Template to use (default: "default")
Returns:
str: Success message or error details
"""
try:
response = get_unity_connection().send_command("CREATE_PREFAB", {
"prefab_name": prefab_name,
"template": template
})
return response.get("message", "Prefab created successfully")
except Exception as e:
return f"Error creating prefab: {str(e)}"
```
3. **Update Prompt**:
```python
"1. **Prefab Management**:\n"
" - Create prefabs with `create_prefab(prefab_name, template)`\n"
```
## Troubleshooting
If you encounter issues:
1. Check the Unity Console for C# errors
2. Verify the command name matches between Python and C#
3. Ensure all parameters are properly serialized
4. Check the Python logs for connection issues
5. Verify the tool is properly registered in both environments

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ac49e223d2e7f2f41bac3ea65cab4e1e
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Python.meta Normal file
View File

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

1
Python/.python-version Normal file
View File

@ -0,0 +1 @@
3.12

3
Python/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""
Unity MCP Server package.
"""

7
Python/__init__.py.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0ac2beb1227032f488e5ed169f517601
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Python/__pycache__.meta Normal file
View File

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

76
Python/config.py Normal file
View File

@ -0,0 +1,76 @@
"""
Configuration settings for the Unity MCP Server.
This file contains all configurable parameters for the server.
"""
from dataclasses import dataclass
from typing import Dict, Any
import json
import os
@dataclass
class ServerConfig:
"""Main configuration class for the MCP server."""
# Network settings
unity_host: str
unity_port: int
mcp_port: int
# Connection settings
connection_timeout: float
buffer_size: int
# Logging settings
log_level: str
log_format: str
# Server settings
max_retries: int
retry_delay: float
@classmethod
def from_file(cls, config_path: str = None) -> "ServerConfig":
"""Load configuration from a JSON file."""
if config_path is None:
# Get the directory where this file is located
current_dir = os.path.dirname(os.path.abspath(__file__))
# Go up one directory to find config.json
config_path = os.path.join(os.path.dirname(current_dir), "config.json")
if not os.path.exists(config_path):
raise FileNotFoundError(f"Configuration file not found at {config_path}. Please ensure config.json exists.")
with open(config_path, 'r') as f:
config_dict = json.load(f)
return cls(**config_dict)
def to_file(self, config_path: str = None) -> None:
"""Save configuration to a JSON file."""
if config_path is None:
# Get the directory where this file is located
current_dir = os.path.dirname(os.path.abspath(__file__))
# Go up one directory to find config.json
config_path = os.path.join(os.path.dirname(current_dir), "config.json")
config_dict = {
"unity_host": self.unity_host,
"unity_port": self.unity_port,
"mcp_port": self.mcp_port,
"connection_timeout": self.connection_timeout,
"buffer_size": self.buffer_size,
"log_level": self.log_level,
"log_format": self.log_format,
"max_retries": self.max_retries,
"retry_delay": self.retry_delay
}
with open(config_path, 'w') as f:
json.dump(config_dict, f, indent=4)
# Create a global config instance
try:
config = ServerConfig.from_file()
except FileNotFoundError as e:
print(f"Error: {e}")
print("Please ensure config.json exists in the Assets/MCPServer directory")
raise

7
Python/config.py.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 542818bee8818c247a4686790159584f
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

10
Python/pyproject.toml Normal file
View File

@ -0,0 +1,10 @@
[project]
name = "unity"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.28.1",
"mcp[cli]>=1.4.1",
]

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 42099f8e69b989f4eab2e84da2f89f48
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

3
Python/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
uvicorn
mcp
fastapi

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 92b0d2ed1524b6747b2c34fe5a3d1a62
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

95
Python/server.py Normal file
View File

@ -0,0 +1,95 @@
from mcp.server.fastmcp import FastMCP, Context, Image
import logging
from dataclasses import dataclass
from contextlib import asynccontextmanager
from typing import AsyncIterator, Dict, Any, List
from config import config
from tools import register_all_tools
from unity_connection import get_unity_connection, UnityConnection
# Configure logging using settings from config
logging.basicConfig(
level=getattr(logging, config.log_level),
format=config.log_format
)
logger = logging.getLogger("UnityMCPServer")
@asynccontextmanager
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
"""Handle server startup and shutdown."""
logger.info("UnityMCP server starting up")
try:
unity = get_unity_connection()
logger.info("Connected to Unity on startup")
except Exception as e:
logger.warning(f"Could not connect to Unity on startup: {str(e)}")
try:
yield {}
finally:
global _unity_connection
if _unity_connection:
_unity_connection.disconnect()
_unity_connection = None
logger.info("UnityMCP server shut down")
# Initialize MCP server
mcp = FastMCP(
"UnityMCP",
description="Unity Editor integration via Model Context Protocol",
lifespan=server_lifespan
)
# Register all tools
register_all_tools(mcp)
# Asset Creation Strategy
@mcp.prompt()
def asset_creation_strategy() -> str:
"""Guide for creating and managing assets in Unity."""
return (
"Unity MCP Server Tools and Best Practices:\n\n"
"1. **Editor Control**\n"
" - `editor_action` - Performs editor-wide actions such as `PLAY`, `PAUSE`, `STOP`, `BUILD`, `SAVE`\n"
"2. **Scene Management**\n"
" - `get_current_scene()`, `get_scene_list()` - Get scene details\n"
" - `open_scene(path)`, `save_scene(path)` - Open/save scenes\n"
" - `new_scene(path)`, `change_scene(path, save_current)` - Create/switch scenes\n\n"
"3. **Object Management**\n"
" - `create_object(name, type)` - Create objects (e.g. `CUBE`, `SPHERE`, `EMPTY`, `CAMERA`)\n"
" - `delete_object(name)` - Remove objects\n"
" - `set_object_transform(name, location, rotation, scale)` - Modify object position, rotation, and scale\n"
" - `add_component(name, component_type)` - Add components to objects (e.g. `Rigidbody`, `BoxCollider`)\n"
" - `remove_component(name, component_type)` - Remove components from objects\n"
" - `get_object_properties(name)` - Get object properties\n"
" - `find_objects_by_name(name)` - Find objects by name\n"
" - `get_hierarchy()` - Get object hierarchy\n"
"4. **Script Management**\n"
" - `create_script(name, type, namespace, template)` - Create scripts\n"
" - `view_script(path)`, `update_script(path, content)` - View/modify scripts\n"
" - `attach_script(object_name, script_name)` - Add scripts to objects\n"
" - `list_scripts(folder_path)` - List scripts in folder\n\n"
"5. **Asset Management**\n"
" - `import_asset(source_path, target_path)` - Import external assets\n"
" - `instantiate_prefab(path, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z)` - Create prefab instances\n"
" - `create_prefab(object_name, path)`, `apply_prefab(object_name, path)` - Manage prefabs\n"
" - `get_asset_list(type, search_pattern, folder)` - List project assets\n"
" - Use relative paths for Unity assets (e.g., 'Assets/Models/MyModel.fbx')\n"
" - Use absolute paths for external files\n\n"
"6. **Material Management**\n"
" - `set_material(object_name, material_name, color)` - Apply/create materials\n"
" - Use RGB colors (0.0-1.0 range)\n\n"
"7. **Best Practices**\n"
" - Use meaningful names for objects and scripts\n"
" - Keep scripts organized in folders with namespaces\n"
" - Verify changes after modifications\n"
" - Save scenes before major changes\n"
" - Use full component names (e.g., 'Rigidbody', 'BoxCollider')\n"
" - Provide correct value types for properties\n"
" - Keep prefabs in dedicated folders\n"
" - Regularly apply prefab changes\n"
)
# Run the server
if __name__ == "__main__":
mcp.run(transport='stdio')

7
Python/server.py.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1da9d93169021574c8718cb028bc00d0
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Python/tools.meta Normal file
View File

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

15
Python/tools/__init__.py Normal file
View File

@ -0,0 +1,15 @@
from .scene_tools import register_scene_tools
from .script_tools import register_script_tools
from .material_tools import register_material_tools
from .editor_tools import register_editor_tools
from .asset_tools import register_asset_tools
from .object_tools import register_object_tools
def register_all_tools(mcp):
"""Register all tools with the MCP server."""
register_scene_tools(mcp)
register_script_tools(mcp)
register_material_tools(mcp)
register_editor_tools(mcp)
register_asset_tools(mcp)
register_object_tools(mcp)

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d46ca128348e4974dbf321531089c622
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

166
Python/tools/asset_tools.py Normal file
View File

@ -0,0 +1,166 @@
from typing import Optional
from mcp.server.fastmcp import FastMCP, Context
from unity_connection import get_unity_connection
def register_asset_tools(mcp: FastMCP):
"""Register all asset management tools with the MCP server."""
@mcp.tool()
def import_asset(
ctx: Context,
source_path: str,
target_path: str
) -> str:
"""Import an asset (e.g., 3D model, texture) into the Unity project.
Args:
ctx: The MCP context
source_path: Path to the source file on disk
target_path: Path where the asset should be imported in the Unity project (relative to Assets folder)
Returns:
str: Success message or error details
"""
try:
# Parameter validation
if not source_path or not isinstance(source_path, str):
return f"Error importing asset: source_path must be a valid string"
if not target_path or not isinstance(target_path, str):
return f"Error importing asset: target_path must be a valid string"
response = get_unity_connection().send_command("IMPORT_ASSET", {
"source_path": source_path,
"target_path": target_path
})
if not response.get("success", False):
return f"Error importing asset: {response.get('error', 'Unknown error')} (Source: {source_path}, Target: {target_path})"
return response.get("message", "Asset imported successfully")
except Exception as e:
return f"Error importing asset: {str(e)} (Source: {source_path}, Target: {target_path})"
@mcp.tool()
def instantiate_prefab(
ctx: Context,
prefab_path: str,
position_x: float = 0.0,
position_y: float = 0.0,
position_z: float = 0.0,
rotation_x: float = 0.0,
rotation_y: float = 0.0,
rotation_z: float = 0.0
) -> str:
"""Instantiate a prefab into the current scene at a specified location.
Args:
ctx: The MCP context
prefab_path: Path to the prefab asset (relative to Assets folder)
position_x: X position in world space (default: 0.0)
position_y: Y position in world space (default: 0.0)
position_z: Z position in world space (default: 0.0)
rotation_x: X rotation in degrees (default: 0.0)
rotation_y: Y rotation in degrees (default: 0.0)
rotation_z: Z rotation in degrees (default: 0.0)
Returns:
str: Success message or error details
"""
try:
# Parameter validation
if not prefab_path or not isinstance(prefab_path, str):
return f"Error instantiating prefab: prefab_path must be a valid string"
# Validate numeric parameters
position_params = {
"position_x": position_x,
"position_y": position_y,
"position_z": position_z,
"rotation_x": rotation_x,
"rotation_y": rotation_y,
"rotation_z": rotation_z
}
for param_name, param_value in position_params.items():
if not isinstance(param_value, (int, float)):
return f"Error instantiating prefab: {param_name} must be a number"
response = get_unity_connection().send_command("INSTANTIATE_PREFAB", {
"prefab_path": prefab_path,
"position_x": position_x,
"position_y": position_y,
"position_z": position_z,
"rotation_x": rotation_x,
"rotation_y": rotation_y,
"rotation_z": rotation_z
})
if not response.get("success", False):
return f"Error instantiating prefab: {response.get('error', 'Unknown error')} (Path: {prefab_path})"
return f"Prefab instantiated successfully as '{response.get('instance_name', 'unknown')}'"
except Exception as e:
return f"Error instantiating prefab: {str(e)} (Path: {prefab_path})"
@mcp.tool()
def create_prefab(
ctx: Context,
object_name: str,
prefab_path: str
) -> str:
"""Create a new prefab asset from a GameObject in the scene.
Args:
ctx: The MCP context
object_name: Name of the GameObject in the scene to create prefab from
prefab_path: Path where the prefab should be saved (relative to Assets folder)
Returns:
str: Success message or error details
"""
try:
# Parameter validation
if not object_name or not isinstance(object_name, str):
return f"Error creating prefab: object_name must be a valid string"
if not prefab_path or not isinstance(prefab_path, str):
return f"Error creating prefab: prefab_path must be a valid string"
# Verify prefab path has proper extension
if not prefab_path.lower().endswith('.prefab'):
prefab_path = f"{prefab_path}.prefab"
response = get_unity_connection().send_command("CREATE_PREFAB", {
"object_name": object_name,
"prefab_path": prefab_path
})
if not response.get("success", False):
return f"Error creating prefab: {response.get('error', 'Unknown error')} (Object: {object_name}, Path: {prefab_path})"
return f"Prefab created successfully at {response.get('path', prefab_path)}"
except Exception as e:
return f"Error creating prefab: {str(e)} (Object: {object_name}, Path: {prefab_path})"
@mcp.tool()
def apply_prefab(
ctx: Context,
object_name: str
) -> str:
"""Apply changes made to a prefab instance back to the original prefab asset.
Args:
ctx: The MCP context
object_name: Name of the prefab instance in the scene
Returns:
str: Success message or error details
"""
try:
response = get_unity_connection().send_command("APPLY_PREFAB", {
"object_name": object_name
})
return response.get("message", "Prefab changes applied successfully")
except Exception as e:
return f"Error applying prefab changes: {str(e)}"

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 92e45409008a25a458c9e9e3e27c5131
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,125 @@
from mcp.server.fastmcp import FastMCP, Context
from typing import Optional
from unity_connection import get_unity_connection
def register_editor_tools(mcp: FastMCP):
"""Register all editor control tools with the MCP server."""
@mcp.tool()
def undo(ctx: Context) -> str:
"""Undo the last action performed in the Unity editor.
Returns:
str: Success message or error details
"""
try:
response = get_unity_connection().send_command("EDITOR_CONTROL", {
"command": "UNDO"
})
return response.get("message", "Undo performed successfully")
except Exception as e:
return f"Error performing undo: {str(e)}"
@mcp.tool()
def redo(ctx: Context) -> str:
"""Redo the last undone action in the Unity editor.
Returns:
str: Success message or error details
"""
try:
response = get_unity_connection().send_command("EDITOR_CONTROL", {
"command": "REDO"
})
return response.get("message", "Redo performed successfully")
except Exception as e:
return f"Error performing redo: {str(e)}"
@mcp.tool()
def play(ctx: Context) -> str:
"""Start the game in play mode within the Unity editor.
Returns:
str: Success message or error details
"""
try:
response = get_unity_connection().send_command("EDITOR_CONTROL", {
"command": "PLAY"
})
return response.get("message", "Entered play mode")
except Exception as e:
return f"Error entering play mode: {str(e)}"
@mcp.tool()
def pause(ctx: Context) -> str:
"""Pause the game while in play mode.
Returns:
str: Success message or error details
"""
try:
response = get_unity_connection().send_command("EDITOR_CONTROL", {
"command": "PAUSE"
})
return response.get("message", "Game paused")
except Exception as e:
return f"Error pausing game: {str(e)}"
@mcp.tool()
def stop(ctx: Context) -> str:
"""Stop the game and exit play mode.
Returns:
str: Success message or error details
"""
try:
response = get_unity_connection().send_command("EDITOR_CONTROL", {
"command": "STOP"
})
return response.get("message", "Exited play mode")
except Exception as e:
return f"Error stopping game: {str(e)}"
@mcp.tool()
def build(ctx: Context, platform: str, build_path: str) -> str:
"""Build the project for a specified platform.
Args:
platform: Target platform (windows, mac, linux, android, ios, webgl)
build_path: Path where the build should be saved
Returns:
str: Success message or error details
"""
try:
response = get_unity_connection().send_command("EDITOR_CONTROL", {
"command": "BUILD",
"params": {
"platform": platform,
"buildPath": build_path
}
})
return response.get("message", "Build completed successfully")
except Exception as e:
return f"Error building project: {str(e)}"
@mcp.tool()
def execute_command(ctx: Context, command_name: str) -> str:
"""Execute a specific editor command or custom script within the Unity editor.
Args:
command_name: Name of the editor command to execute (e.g., "Edit/Preferences")
Returns:
str: Success message or error details
"""
try:
response = get_unity_connection().send_command("EDITOR_CONTROL", {
"command": "EXECUTE_COMMAND",
"params": {
"commandName": command_name
}
})
return response.get("message", f"Executed command: {command_name}")
except Exception as e:
return f"Error executing command: {str(e)}"

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: bed91baa79f8eba4e8e3524743d5bc61
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,33 @@
from mcp.server.fastmcp import FastMCP, Context
from typing import List
from unity_connection import get_unity_connection
def register_material_tools(mcp: FastMCP):
"""Register all material-related tools with the MCP server."""
@mcp.tool()
def set_material(
ctx: Context,
object_name: str,
material_name: str = None,
color: List[float] = None
) -> str:
"""
Apply or create a material for a game object.
Args:
object_name: Target game object.
material_name: Optional material name.
color: Optional [R, G, B] values (0.0-1.0).
"""
try:
unity = get_unity_connection()
params = {"object_name": object_name}
if material_name:
params["material_name"] = material_name
if color:
params["color"] = color
result = unity.send_command("SET_MATERIAL", params)
return f"Applied material to {object_name}: {result.get('material_name', 'unknown')}"
except Exception as e:
return f"Error setting material: {str(e)}"

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 58086e81be6fdc6488db4601949f8872
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,197 @@
"""Tools for inspecting and manipulating Unity objects."""
from typing import Optional, List, Dict, Any
from mcp.server.fastmcp import FastMCP, Context
from unity_connection import get_unity_connection
def register_object_tools(mcp: FastMCP):
"""Register all object inspection and manipulation tools with the MCP server."""
@mcp.tool()
def get_object_properties(
ctx: Context,
name: str
) -> Dict[str, Any]:
"""Get all properties of a specified game object.
Args:
ctx: The MCP context
name: Name of the game object to inspect
Returns:
Dict containing the object's properties, components, and their values
"""
try:
response = get_unity_connection().send_command("GET_OBJECT_PROPERTIES", {
"name": name
})
return response
except Exception as e:
return {"error": f"Failed to get object properties: {str(e)}"}
@mcp.tool()
def get_component_properties(
ctx: Context,
object_name: str,
component_type: str
) -> Dict[str, Any]:
"""Get properties of a specific component on a game object.
Args:
ctx: The MCP context
object_name: Name of the game object
component_type: Type of the component to inspect
Returns:
Dict containing the component's properties and their values
"""
try:
response = get_unity_connection().send_command("GET_COMPONENT_PROPERTIES", {
"object_name": object_name,
"component_type": component_type
})
return response
except Exception as e:
return {"error": f"Failed to get component properties: {str(e)}"}
@mcp.tool()
def find_objects_by_name(
ctx: Context,
name: str
) -> List[Dict[str, str]]:
"""Find game objects in the scene by name.
Args:
ctx: The MCP context
name: Name to search for (partial matches are supported)
Returns:
List of dicts containing object names and their paths
"""
try:
response = get_unity_connection().send_command("FIND_OBJECTS_BY_NAME", {
"name": name
})
return response.get("objects", [])
except Exception as e:
return [{"error": f"Failed to find objects: {str(e)}"}]
@mcp.tool()
def find_objects_by_tag(
ctx: Context,
tag: str
) -> List[Dict[str, str]]:
"""Find game objects in the scene by tag.
Args:
ctx: The MCP context
tag: Tag to search for
Returns:
List of dicts containing object names and their paths
"""
try:
response = get_unity_connection().send_command("FIND_OBJECTS_BY_TAG", {
"tag": tag
})
return response.get("objects", [])
except Exception as e:
return [{"error": f"Failed to find objects: {str(e)}"}]
@mcp.tool()
def get_scene_info(ctx: Context) -> Dict[str, Any]:
"""Get information about the current scene.
Args:
ctx: The MCP context
Returns:
Dict containing scene information including name and root objects
"""
try:
response = get_unity_connection().send_command("GET_SCENE_INFO")
return response
except Exception as e:
return {"error": f"Failed to get scene info: {str(e)}"}
@mcp.tool()
def get_hierarchy(ctx: Context) -> Dict[str, Any]:
"""Get the current hierarchy of game objects in the scene.
Args:
ctx: The MCP context
Returns:
Dict containing the scene hierarchy as a tree structure
"""
try:
response = get_unity_connection().send_command("GET_HIERARCHY")
return response
except Exception as e:
return {"error": f"Failed to get hierarchy: {str(e)}"}
@mcp.tool()
def select_object(
ctx: Context,
name: str
) -> Dict[str, str]:
"""Select a game object in the Unity Editor.
Args:
ctx: The MCP context
name: Name of the object to select
Returns:
Dict containing the name of the selected object
"""
try:
response = get_unity_connection().send_command("SELECT_OBJECT", {
"name": name
})
return response
except Exception as e:
return {"error": f"Failed to select object: {str(e)}"}
@mcp.tool()
def get_selected_object(ctx: Context) -> Optional[Dict[str, str]]:
"""Get the currently selected game object in the Unity Editor.
Args:
ctx: The MCP context
Returns:
Dict containing the selected object's name and path, or None if no object is selected
"""
try:
response = get_unity_connection().send_command("GET_SELECTED_OBJECT")
return response.get("selected")
except Exception as e:
return {"error": f"Failed to get selected object: {str(e)}"}
@mcp.tool()
def get_asset_list(
ctx: Context,
type: Optional[str] = None,
search_pattern: str = "*",
folder: str = "Assets"
) -> List[Dict[str, str]]:
"""Get a list of assets in the project.
Args:
ctx: The MCP context
type: Optional asset type to filter by
search_pattern: Pattern to search for in asset names
folder: Folder to search in (default: "Assets")
Returns:
List of dicts containing asset information
"""
try:
response = get_unity_connection().send_command("GET_ASSET_LIST", {
"type": type,
"search_pattern": search_pattern,
"folder": folder
})
return response.get("assets", [])
except Exception as e:
return [{"error": f"Failed to get asset list: {str(e)}"}]

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7b57b4da1500a9f4295cc0761407a6cc
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

227
Python/tools/scene_tools.py Normal file
View File

@ -0,0 +1,227 @@
from mcp.server.fastmcp import FastMCP, Context
from typing import List, Dict, Any, Optional
import json
from unity_connection import get_unity_connection
def register_scene_tools(mcp: FastMCP):
"""Register all scene-related tools with the MCP server."""
@mcp.tool()
def get_scene_info(ctx: Context) -> str:
"""Retrieve detailed info about the current Unity scene."""
try:
unity = get_unity_connection()
result = unity.send_command("GET_SCENE_INFO")
return json.dumps(result, indent=2)
except Exception as e:
return f"Error getting scene info: {str(e)}"
@mcp.tool()
def open_scene(ctx: Context, scene_path: str) -> str:
"""Open a specified scene in the Unity editor.
Args:
scene_path: Full path to the scene file (e.g., "Assets/Scenes/MyScene.unity")
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
result = unity.send_command("OPEN_SCENE", {"scene_path": scene_path})
return result.get("message", "Scene opened successfully")
except Exception as e:
return f"Error opening scene: {str(e)}"
@mcp.tool()
def save_scene(ctx: Context) -> str:
"""Save the current scene to its file.
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
result = unity.send_command("SAVE_SCENE")
return result.get("message", "Scene saved successfully")
except Exception as e:
return f"Error saving scene: {str(e)}"
@mcp.tool()
def new_scene(ctx: Context, scene_path: str) -> str:
"""Create a new empty scene in the Unity editor.
Args:
scene_path: Full path where the new scene should be saved (e.g., "Assets/Scenes/NewScene.unity")
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
# Create new scene
result = unity.send_command("NEW_SCENE", {"scene_path": scene_path})
# Save the scene to ensure it's properly created
unity.send_command("SAVE_SCENE")
# Get scene info to verify it's loaded
scene_info = unity.send_command("GET_SCENE_INFO")
return result.get("message", "New scene created successfully")
except Exception as e:
return f"Error creating new scene: {str(e)}"
@mcp.tool()
def change_scene(ctx: Context, scene_path: str, save_current: bool = False) -> str:
"""Change to a different scene, optionally saving the current one.
Args:
scene_path: Full path to the target scene file (e.g., "Assets/Scenes/TargetScene.unity")
save_current: Whether to save the current scene before changing (default: False)
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
result = unity.send_command("CHANGE_SCENE", {
"scene_path": scene_path,
"save_current": save_current
})
return result.get("message", "Scene changed successfully")
except Exception as e:
return f"Error changing scene: {str(e)}"
@mcp.tool()
def get_object_info(ctx: Context, object_name: str) -> str:
"""
Get info about a specific game object.
Args:
object_name: Name of the game object.
"""
try:
unity = get_unity_connection()
result = unity.send_command("GET_OBJECT_INFO", {"name": object_name})
return json.dumps(result, indent=2)
except Exception as e:
return f"Error getting object info: {str(e)}"
@mcp.tool()
def create_object(
ctx: Context,
type: str = "CUBE",
name: str = None,
location: List[float] = None,
rotation: List[float] = None,
scale: List[float] = None
) -> str:
"""
Create a game object in the Unity scene.
Args:
type: Object type (CUBE, SPHERE, CYLINDER, CAPSULE, PLANE, EMPTY, CAMERA, LIGHT).
name: Optional name for the game object.
location: [x, y, z] position (defaults to [0, 0, 0]).
rotation: [x, y, z] rotation in degrees (defaults to [0, 0, 0]).
scale: [x, y, z] scale factors (defaults to [1, 1, 1]).
Returns:
Confirmation message with the created object's name.
"""
try:
unity = get_unity_connection()
params = {
"type": type.upper(),
"location": location or [0, 0, 0],
"rotation": rotation or [0, 0, 0],
"scale": scale or [1, 1, 1]
}
if name:
params["name"] = name
result = unity.send_command("CREATE_OBJECT", params)
return f"Created {type} game object: {result['name']}"
except Exception as e:
return f"Error creating game object: {str(e)}"
@mcp.tool()
def modify_object(
ctx: Context,
name: str,
location: Optional[List[float]] = None,
rotation: Optional[List[float]] = None,
scale: Optional[List[float]] = None,
visible: Optional[bool] = None,
set_parent: Optional[str] = None,
add_component: Optional[str] = None,
remove_component: Optional[str] = None,
set_property: Optional[Dict[str, Any]] = None
) -> str:
"""
Modify a game object's properties and components.
Args:
name: Name of the game object to modify.
location: Optional [x, y, z] position.
rotation: Optional [x, y, z] rotation in degrees.
scale: Optional [x, y, z] scale factors.
visible: Optional visibility toggle.
set_parent: Optional name of the parent object to set.
add_component: Optional name of the component type to add (e.g., "Rigidbody", "BoxCollider").
remove_component: Optional name of the component type to remove.
set_property: Optional dict with keys:
- component: Name of the component type
- property: Name of the property to set
- value: Value to set the property to
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
params = {"name": name}
# Add basic transform properties
if location is not None:
params["location"] = location
if rotation is not None:
params["rotation"] = rotation
if scale is not None:
params["scale"] = scale
if visible is not None:
params["visible"] = visible
# Add parent setting
if set_parent is not None:
params["set_parent"] = set_parent
# Add component operations
if add_component is not None:
params["add_component"] = add_component
if remove_component is not None:
params["remove_component"] = remove_component
# Add property setting
if set_property is not None:
params["set_property"] = set_property
result = unity.send_command("MODIFY_OBJECT", params)
return f"Modified game object: {result['name']}"
except Exception as e:
return f"Error modifying game object: {str(e)}"
@mcp.tool()
def delete_object(ctx: Context, name: str) -> str:
"""
Remove a game object from the scene.
Args:
name: Name of the game object to delete.
"""
try:
unity = get_unity_connection()
result = unity.send_command("DELETE_OBJECT", {"name": name})
return f"Deleted game object: {name}"
except Exception as e:
return f"Error deleting game object: {str(e)}"

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d535fb5dfea28e9499814a523eb6d4c6
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,133 @@
from mcp.server.fastmcp import FastMCP, Context
from typing import List
from unity_connection import get_unity_connection
def register_script_tools(mcp: FastMCP):
"""Register all script-related tools with the MCP server."""
@mcp.tool()
def view_script(ctx: Context, script_path: str) -> str:
"""View the contents of a Unity script file.
Args:
ctx: The MCP context
script_path: Path to the script file relative to the Assets folder
Returns:
str: The contents of the script file or error message
"""
try:
# Send command to Unity to read the script file
response = get_unity_connection().send_command("VIEW_SCRIPT", {
"script_path": script_path
})
return response.get("content", "Script not found")
except Exception as e:
return f"Error viewing script: {str(e)}"
@mcp.tool()
def create_script(
ctx: Context,
script_name: str,
script_type: str = "MonoBehaviour",
namespace: str = None,
template: str = None
) -> str:
"""Create a new Unity script file.
Args:
ctx: The MCP context
script_name: Name of the script (without .cs extension)
script_type: Type of script (e.g., MonoBehaviour, ScriptableObject)
namespace: Optional namespace for the script
template: Optional custom template to use
Returns:
str: Success message or error details
"""
try:
# Send command to Unity to create the script
response = get_unity_connection().send_command("CREATE_SCRIPT", {
"script_name": script_name,
"script_type": script_type,
"namespace": namespace,
"template": template
})
return response.get("message", "Script created successfully")
except Exception as e:
return f"Error creating script: {str(e)}"
@mcp.tool()
def update_script(
ctx: Context,
script_path: str,
content: str
) -> str:
"""Update the contents of an existing Unity script.
Args:
ctx: The MCP context
script_path: Path to the script file relative to the Assets folder
content: New content for the script
Returns:
str: Success message or error details
"""
try:
# Send command to Unity to update the script
response = get_unity_connection().send_command("UPDATE_SCRIPT", {
"script_path": script_path,
"content": content
})
return response.get("message", "Script updated successfully")
except Exception as e:
return f"Error updating script: {str(e)}"
@mcp.tool()
def list_scripts(ctx: Context, folder_path: str = "Assets") -> str:
"""List all script files in a specified folder.
Args:
ctx: The MCP context
folder_path: Path to the folder to search (default: Assets)
Returns:
str: List of script files or error message
"""
try:
# Send command to Unity to list scripts
response = get_unity_connection().send_command("LIST_SCRIPTS", {
"folder_path": folder_path
})
scripts = response.get("scripts", [])
if not scripts:
return "No scripts found in the specified folder"
return "\n".join(scripts)
except Exception as e:
return f"Error listing scripts: {str(e)}"
@mcp.tool()
def attach_script(
ctx: Context,
object_name: str,
script_name: str
) -> str:
"""Attach a script component to a GameObject.
Args:
ctx: The MCP context
object_name: Name of the target GameObject in the scene
script_name: Name of the script to attach (with or without .cs extension)
Returns:
str: Success message or error details
"""
try:
# Send command to Unity to attach the script
response = get_unity_connection().send_command("ATTACH_SCRIPT", {
"object_name": object_name,
"script_name": script_name
})
return response.get("message", "Script attached successfully")
except Exception as e:
return f"Error attaching script: {str(e)}"

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 41163196b9694f541ab63f3b13813b2e
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

184
Python/unity_connection.py Normal file
View File

@ -0,0 +1,184 @@
import socket
import json
import logging
from dataclasses import dataclass
from typing import Dict, Any
from config import config
# Configure logging using settings from config
logging.basicConfig(
level=getattr(logging, config.log_level),
format=config.log_format
)
logger = logging.getLogger("UnityMCPServer")
@dataclass
class UnityConnection:
"""Manages the socket connection to the Unity Editor."""
host: str = config.unity_host
port: int = config.unity_port
sock: socket.socket = None # Socket for Unity communication
def connect(self) -> bool:
"""Establish a connection to the Unity Editor."""
if self.sock:
return True
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.host, self.port))
logger.info(f"Connected to Unity at {self.host}:{self.port}")
return True
except Exception as e:
logger.error(f"Failed to connect to Unity: {str(e)}")
self.sock = None
return False
def disconnect(self):
"""Close the connection to the Unity Editor."""
if self.sock:
try:
self.sock.close()
except Exception as e:
logger.error(f"Error disconnecting from Unity: {str(e)}")
finally:
self.sock = None
def receive_full_response(self, sock, buffer_size=config.buffer_size) -> bytes:
"""Receive a complete response from Unity, handling chunked data."""
chunks = []
sock.settimeout(config.connection_timeout) # Use timeout from config
try:
while True:
chunk = sock.recv(buffer_size)
if not chunk:
if not chunks:
raise Exception("Connection closed before receiving data")
break
chunks.append(chunk)
# Process the data received so far
data = b''.join(chunks)
decoded_data = data.decode('utf-8')
# Check if we've received a complete response
try:
# Special case for ping-pong
if decoded_data.strip().startswith('{"status":"success","result":{"message":"pong"'):
logger.debug("Received ping response")
return data
# Handle escaped quotes in the content
if '"content":' in decoded_data:
# Find the content field and its value
content_start = decoded_data.find('"content":') + 9
content_end = decoded_data.rfind('"', content_start)
if content_end > content_start:
# Replace escaped quotes in content with regular quotes
content = decoded_data[content_start:content_end]
content = content.replace('\\"', '"')
decoded_data = decoded_data[:content_start] + content + decoded_data[content_end:]
# Validate JSON format
json.loads(decoded_data)
# If we get here, we have valid JSON
logger.info(f"Received complete response ({len(data)} bytes)")
return data
except json.JSONDecodeError:
# We haven't received a complete valid JSON response yet
continue
except Exception as e:
logger.warning(f"Error processing response chunk: {str(e)}")
# Continue reading more chunks as this might not be the complete response
continue
except socket.timeout:
logger.warning("Socket timeout during receive")
raise Exception("Timeout receiving Unity response")
except Exception as e:
logger.error(f"Error during receive: {str(e)}")
raise
def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
"""Send a command to Unity and return its response."""
if not self.sock and not self.connect():
raise ConnectionError("Not connected to Unity")
# Special handling for ping command
if command_type == "ping":
try:
logger.debug("Sending ping to verify connection")
self.sock.sendall(b"ping")
response_data = self.receive_full_response(self.sock)
response = json.loads(response_data.decode('utf-8'))
if response.get("status") != "success":
logger.warning("Ping response was not successful")
self.sock = None
raise ConnectionError("Connection verification failed")
return {"message": "pong"}
except Exception as e:
logger.error(f"Ping error: {str(e)}")
self.sock = None
raise ConnectionError(f"Connection verification failed: {str(e)}")
# Normal command handling
command = {"type": command_type, "params": params or {}}
try:
logger.info(f"Sending command: {command_type} with params: {params}")
self.sock.sendall(json.dumps(command).encode('utf-8'))
response_data = self.receive_full_response(self.sock)
response = json.loads(response_data.decode('utf-8'))
if response.get("status") == "error":
error_message = response.get("error") or response.get("message", "Unknown Unity error")
logger.error(f"Unity error: {error_message}")
raise Exception(error_message)
return response.get("result", {})
except Exception as e:
logger.error(f"Communication error with Unity: {str(e)}")
self.sock = None
raise Exception(f"Failed to communicate with Unity: {str(e)}")
# Global Unity connection
_unity_connection = None
def get_unity_connection() -> UnityConnection:
"""Retrieve or establish a persistent Unity connection."""
global _unity_connection
if _unity_connection is not None:
try:
# Try to ping with a short timeout to verify connection
result = _unity_connection.send_command("ping")
# If we get here, the connection is still valid
logger.debug("Reusing existing Unity connection")
return _unity_connection
except Exception as e:
logger.warning(f"Existing connection failed: {str(e)}")
try:
_unity_connection.disconnect()
except:
pass
_unity_connection = None
# Create a new connection
logger.info("Creating new Unity connection")
_unity_connection = UnityConnection()
if not _unity_connection.connect():
_unity_connection = None
raise ConnectionError("Could not connect to Unity. Ensure the Unity Editor and MCP Bridge are running.")
try:
# Verify the new connection works
_unity_connection.send_command("ping")
logger.info("Successfully established new Unity connection")
return _unity_connection
except Exception as e:
logger.error(f"Could not verify new connection: {str(e)}")
try:
_unity_connection.disconnect()
except:
pass
_unity_connection = None
raise ConnectionError(f"Could not establish valid Unity connection: {str(e)}")

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a011d736b5856704a92fdad142dc5982
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

349
Python/uv.lock Normal file
View File

@ -0,0 +1,349 @@
version = 1
revision = 1
requires-python = ">=3.12"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
]
[[package]]
name = "certifi"
version = "2025.1.31"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]]
name = "httpcore"
version = "1.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[[package]]
name = "httpx-sse"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
]
[[package]]
name = "mcp"
version = "1.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpx" },
{ name = "httpx-sse" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "sse-starlette" },
{ name = "starlette" },
{ name = "uvicorn" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/cc/5c5bb19f1a0f8f89a95e25cb608b0b07009e81fd4b031e519335404e1422/mcp-1.4.1.tar.gz", hash = "sha256:b9655d2de6313f9d55a7d1df62b3c3fe27a530100cc85bf23729145b0dba4c7a", size = 154942 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/0e/885f156ade60108e67bf044fada5269da68e29d758a10b0c513f4d85dd76/mcp-1.4.1-py3-none-any.whl", hash = "sha256:a7716b1ec1c054e76f49806f7d96113b99fc1166fc9244c2c6f19867cb75b593", size = 72448 },
]
[package.optional-dependencies]
cli = [
{ name = "python-dotenv" },
{ name = "typer" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
]
[[package]]
name = "pydantic"
version = "2.10.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
]
[[package]]
name = "pydantic-core"
version = "2.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
]
[[package]]
name = "pydantic-settings"
version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 },
]
[[package]]
name = "pygments"
version = "2.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "rich"
version = "13.9.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "sse-starlette"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "starlette" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 },
]
[[package]]
name = "starlette"
version = "0.46.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 },
]
[[package]]
name = "typer"
version = "0.15.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "unity"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "httpx" },
{ name = "mcp", extra = ["cli"] },
]
[package.metadata]
requires-dist = [
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "mcp", extras = ["cli"], specifier = ">=1.4.1" },
]
[[package]]
name = "uvicorn"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
]

7
Python/uv.lock.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: edd19cc45407b714390fae884dca4588
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

81
README.md Normal file
View File

@ -0,0 +1,81 @@
# Unity Model Context Protocol (MCP) Server
A bridge between Python and Unity that allows for programmatic control of the Unity Editor through Python scripts.
## Overview
The Unity MCP Server provides a bidirectional communication channel between Python and the Unity Editor, enabling:
- Creation and manipulation of Unity assets
- Scene management and object manipulation
- Material and script editing
- Editor control and automation
This system is designed to make Unity Editor operations programmable through Python, allowing for more complex automation workflows and integrations.
## Structure
- **Editor/**: C# implementation of Unity-side command handlers
- **Commands/**: Command handlers organized by functionality
- **Models/**: Data models and contract definitions
- **Helpers/**: Utility classes for common operations
- **MCPServerWindow.cs**: Unity Editor window for controlling the MCP Server
- **UnityMCPBridge.cs**: Core communication bridge implementation
- **Python/**: Python server implementation
- **tools/**: Python tool implementations that map to Unity commands
- **server.py**: FastAPI server implementation
- **unity_connection.py**: Communication layer for Unity connection
## Installation
1. Import this package into your Unity project
2. Install Python requirements:
```bash
cd Assets/MCPServer/Python
pip install -e .
```
## Usage
1. Set up MCP integration in Unity:
- Open Window > Unity MCP
- Click the configuration button to set up integration with MCP clients like Claude Desktop or Cursor
2. The Unity Bridge will start automatically when the Unity Editor launches, and the Python server will be started by the MCP client when needed.
3. Use Python tools to control Unity through the MCP client:
```python
# Example: Create a new cube in the scene
create_primitive(primitive_type="Cube", position=[0, 0, 0])
# Example: Change material color
set_material_color(material_name="MyMaterial", color=[1, 0, 0, 1])
```
## Adding New Tools
See [HOW_TO_ADD_A_TOOL.md](HOW_TO_ADD_A_TOOL.md) for detailed instructions on extending the MCP Server with your own tools.
## Best Practices
- Always validate parameters on both Python and C# sides
- Use try-catch blocks for error handling in both environments
- Follow the established naming conventions (UPPER_SNAKE_CASE for commands, snake_case for Python tools)
- Group related functionality in appropriate tool modules and command handlers
## Testing
Run Python tests with:
```bash
python -m unittest discover Assets/MCPServer/Python/tests
```
## Troubleshooting
- Check Unity Console for C# errors
- Verify your MCP client (Claude Desktop, Cursor) is properly configured
- Check the MCP integration status in Window > Unity MCP
- Check network connectivity between Unity and the MCP client
- Ensure commands are properly registered in CommandRegistry.cs
- Verify Python tools are properly imported and registered

7
README.md.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3d64429a98f7ca444a268dca0d12bd1a
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

11
config.json Normal file
View File

@ -0,0 +1,11 @@
{
"unity_host": "localhost",
"unity_port": 6400,
"mcp_port": 6500,
"connection_timeout": 15.0,
"buffer_size": 32768,
"log_level": "INFO",
"log_format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"max_retries": 3,
"retry_delay": 1.0
}

7
config.json.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a31fb9a1b075ce14ca73c2fb65a23fdb
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

10
package.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "com.justinpbarnett.unitymcpserver",
"version": "0.1.0",
"displayName": "Unity MCP Server",
"description": "A Unity package to communicate with a local MCP Client via a Python server.",
"unity": "6000.0",
"dependencies": {
"com.unity.nuget.newtonsoft-json": "3.0.2"
}
}

7
package.json.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7365440e2e9736443a0e0286fd1209c7
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: