initial commit
commit
276ac8166f
|
|
@ -0,0 +1,12 @@
|
|||
.codeignore
|
||||
*codeclip*
|
||||
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 21c33447702ba1b4bad0ce38a7063b13
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9e05ec4381296a243b71d739d63ee5e1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e63606bd2cb4e534b9aeeb774a7bb712
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 278afefc8a504e742b68893419a0ec40
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4c3f2560f6bf61f4c8f33e250f381a17
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: de4c089254b61eb47a9d522643177f50
|
||||
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5b576359ba20dd4478ca0b027de9fe57
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dd021dcf9819a0049a5addcafe1e2cb3
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 92d45020dab10bc4f9466aac6f8f3a71
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 816a6e58165e2d843be35ac899d6f716
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d7a0edb5f3b5fdf4b8c21b2634ce869c
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bb251432ab4867d478089cf10b756042
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3c475150178da9d4babcc705db7b8572
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6c357a62c6b79bb45ba78f1f92f0502a
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ac94e422e4e10e1479567416f3bdb67e
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ac49e223d2e7f2f41bac3ea65cab4e1e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 817caff18b21b124f891b71ef48585a5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1 @@
|
|||
3.12
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
Unity MCP Server package.
|
||||
"""
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0ac2beb1227032f488e5ed169f517601
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8f772230b1f92884ca894b0143fddace
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 542818bee8818c247a4686790159584f
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 42099f8e69b989f4eab2e84da2f89f48
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
uvicorn
|
||||
mcp
|
||||
fastapi
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 92b0d2ed1524b6747b2c34fe5a3d1a62
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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')
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1da9d93169021574c8718cb028bc00d0
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 30b461704d14cea488b84870202ae45f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d46ca128348e4974dbf321531089c622
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9be2d1ebb4e9da34481d1e123b9de39a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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)}"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 92e45409008a25a458c9e9e3e27c5131
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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)}"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bed91baa79f8eba4e8e3524743d5bc61
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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)}"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 58086e81be6fdc6488db4601949f8872
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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)}"}]
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7b57b4da1500a9f4295cc0761407a6cc
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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)}"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d535fb5dfea28e9499814a523eb6d4c6
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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)}"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 41163196b9694f541ab63f3b13813b2e
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 85042f0dbe1d9b64ab9c5ef32472e72f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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)}")
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a011d736b5856704a92fdad142dc5982
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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 },
|
||||
]
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: edd19cc45407b714390fae884dca4588
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3d64429a98f7ca444a268dca0d12bd1a
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a31fb9a1b075ce14ca73c2fb65a23fdb
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7365440e2e9736443a0e0286fd1209c7
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue