remove all optional and union and any parameters for cursor
parent
ba4c2a85bf
commit
0b51ff50d5
|
|
@ -30,7 +30,12 @@ namespace UnityMCP.Editor.Tools
|
||||||
// Parameters used by various actions
|
// Parameters used by various actions
|
||||||
JToken targetToken = @params["target"]; // Can be string (name/path) or int (instanceID)
|
JToken targetToken = @params["target"]; // Can be string (name/path) or int (instanceID)
|
||||||
string searchMethod = @params["searchMethod"]?.ToString().ToLower();
|
string searchMethod = @params["searchMethod"]?.ToString().ToLower();
|
||||||
|
|
||||||
|
// Get common parameters (consolidated)
|
||||||
string name = @params["name"]?.ToString();
|
string name = @params["name"]?.ToString();
|
||||||
|
string tag = @params["tag"]?.ToString();
|
||||||
|
string layer = @params["layer"]?.ToString();
|
||||||
|
JToken parentToken = @params["parent"];
|
||||||
|
|
||||||
// --- Prefab Redirection Check ---
|
// --- Prefab Redirection Check ---
|
||||||
string targetPath = targetToken?.Type == JTokenType.String ? targetToken.ToString() : null;
|
string targetPath = targetToken?.Type == JTokenType.String ? targetToken.ToString() : null;
|
||||||
|
|
@ -308,6 +313,21 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set Layer (new for create action)
|
||||||
|
string layerName = @params["layer"]?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(layerName))
|
||||||
|
{
|
||||||
|
int layerId = LayerMask.NameToLayer(layerName);
|
||||||
|
if (layerId != -1)
|
||||||
|
{
|
||||||
|
newGo.layer = layerId;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[ManageGameObject.Create] Layer '{layerName}' not found. Using default layer.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add Components
|
// Add Components
|
||||||
if (@params["componentsToAdd"] is JArray componentsToAddArray)
|
if (@params["componentsToAdd"] is JArray componentsToAddArray)
|
||||||
|
|
@ -438,22 +458,22 @@ namespace UnityMCP.Editor.Tools
|
||||||
|
|
||||||
bool modified = false;
|
bool modified = false;
|
||||||
|
|
||||||
// Rename
|
// Rename (using consolidated 'name' parameter)
|
||||||
string newName = @params["newName"]?.ToString();
|
string name = @params["name"]?.ToString();
|
||||||
if (!string.IsNullOrEmpty(newName) && targetGo.name != newName)
|
if (!string.IsNullOrEmpty(name) && targetGo.name != name)
|
||||||
{
|
{
|
||||||
targetGo.name = newName;
|
targetGo.name = name;
|
||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change Parent
|
// Change Parent (using consolidated 'parent' parameter)
|
||||||
JToken newParentToken = @params["newParent"];
|
JToken parentToken = @params["parent"];
|
||||||
if (newParentToken != null)
|
if (parentToken != null)
|
||||||
{
|
{
|
||||||
GameObject newParentGo = FindObjectInternal(newParentToken, "by_id_or_name_or_path");
|
GameObject newParentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path");
|
||||||
if (newParentGo == null && !(newParentToken.Type == JTokenType.Null || (newParentToken.Type == JTokenType.String && string.IsNullOrEmpty(newParentToken.ToString()))))
|
if (newParentGo == null && !(parentToken.Type == JTokenType.Null || (parentToken.Type == JTokenType.String && string.IsNullOrEmpty(parentToken.ToString()))))
|
||||||
{
|
{
|
||||||
return Response.Error($"New parent ('{newParentToken}') not found.");
|
return Response.Error($"New parent ('{parentToken}') not found.");
|
||||||
}
|
}
|
||||||
// Check for hierarchy loops
|
// Check for hierarchy loops
|
||||||
if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform))
|
if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform))
|
||||||
|
|
@ -475,14 +495,14 @@ namespace UnityMCP.Editor.Tools
|
||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change Tag
|
// Change Tag (using consolidated 'tag' parameter)
|
||||||
string newTag = @params["newTag"]?.ToString();
|
string tag = @params["tag"]?.ToString();
|
||||||
// Only attempt to change tag if a non-null tag is provided and it's different from the current one.
|
// Only attempt to change tag if a non-null tag is provided and it's different from the current one.
|
||||||
// Allow setting an empty string to remove the tag (Unity uses "Untagged").
|
// Allow setting an empty string to remove the tag (Unity uses "Untagged").
|
||||||
if (newTag != null && targetGo.tag != newTag)
|
if (tag != null && targetGo.tag != tag)
|
||||||
{
|
{
|
||||||
// Ensure the tag is not empty, if empty, it means "Untagged" implicitly
|
// Ensure the tag is not empty, if empty, it means "Untagged" implicitly
|
||||||
string tagToSet = string.IsNullOrEmpty(newTag) ? "Untagged" : newTag;
|
string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First attempt to set the tag
|
// First attempt to set the tag
|
||||||
|
|
@ -522,28 +542,19 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change Layer
|
// Change Layer (using consolidated 'layer' parameter)
|
||||||
JToken newLayerToken = @params["newLayer"];
|
string layerName = @params["layer"]?.ToString();
|
||||||
if (newLayerToken != null)
|
if (!string.IsNullOrEmpty(layerName))
|
||||||
{
|
{
|
||||||
int layer = -1;
|
int layerId = LayerMask.NameToLayer(layerName);
|
||||||
if (newLayerToken.Type == JTokenType.Integer)
|
if (layerId == -1 && layerName != "Default")
|
||||||
{
|
{
|
||||||
layer = newLayerToken.ToObject<int>();
|
return Response.Error($"Invalid layer specified: '{layerName}'. Use a valid layer name.");
|
||||||
}
|
}
|
||||||
else if (newLayerToken.Type == JTokenType.String)
|
if (layerId != -1 && targetGo.layer != layerId)
|
||||||
{
|
{
|
||||||
layer = LayerMask.NameToLayer(newLayerToken.ToString());
|
targetGo.layer = layerId;
|
||||||
}
|
modified = true;
|
||||||
|
|
||||||
if (layer == -1 && newLayerToken.ToString() != "Default") // LayerMask.NameToLayer returns -1 for invalid names
|
|
||||||
{
|
|
||||||
return Response.Error($"Invalid layer specified: '{newLayerToken}'. Use a valid layer name or index.");
|
|
||||||
}
|
|
||||||
if (layer != -1 && targetGo.layer != layer)
|
|
||||||
{
|
|
||||||
targetGo.layer = layer;
|
|
||||||
modified = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -999,6 +1010,13 @@ namespace UnityMCP.Editor.Tools
|
||||||
return Response.Error($"Failed to add component '{typeName}' to '{targetGo.name}'. It might be disallowed (e.g., adding script twice).");
|
return Response.Error($"Failed to add component '{typeName}' to '{targetGo.name}'. It might be disallowed (e.g., adding script twice).");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set default values for specific component types
|
||||||
|
if (newComponent is Light light)
|
||||||
|
{
|
||||||
|
// Default newly added lights to directional
|
||||||
|
light.type = LightType.Directional;
|
||||||
|
}
|
||||||
|
|
||||||
// Set properties if provided
|
// Set properties if provided
|
||||||
if (properties != null)
|
if (properties != null)
|
||||||
{
|
{
|
||||||
|
|
@ -1104,6 +1122,13 @@ namespace UnityMCP.Editor.Tools
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Handle special case for materials with dot notation (material.property)
|
||||||
|
// Examples: material.color, sharedMaterial.color, materials[0].color
|
||||||
|
if (memberName.Contains('.') || memberName.Contains('['))
|
||||||
|
{
|
||||||
|
return SetNestedProperty(target, memberName, value);
|
||||||
|
}
|
||||||
|
|
||||||
PropertyInfo propInfo = type.GetProperty(memberName, flags);
|
PropertyInfo propInfo = type.GetProperty(memberName, flags);
|
||||||
if (propInfo != null && propInfo.CanWrite)
|
if (propInfo != null && propInfo.CanWrite)
|
||||||
{
|
{
|
||||||
|
|
@ -1134,6 +1159,265 @@ namespace UnityMCP.Editor.Tools
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a nested property using dot notation (e.g., "material.color") or array access (e.g., "materials[0]")
|
||||||
|
/// </summary>
|
||||||
|
private static bool SetNestedProperty(object target, string path, JToken value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Split the path into parts (handling both dot notation and array indexing)
|
||||||
|
string[] pathParts = SplitPropertyPath(path);
|
||||||
|
if (pathParts.Length == 0) return false;
|
||||||
|
|
||||||
|
object currentObject = target;
|
||||||
|
Type currentType = currentObject.GetType();
|
||||||
|
BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
||||||
|
|
||||||
|
// Traverse the path until we reach the final property
|
||||||
|
for (int i = 0; i < pathParts.Length - 1; i++)
|
||||||
|
{
|
||||||
|
string part = pathParts[i];
|
||||||
|
bool isArray = false;
|
||||||
|
int arrayIndex = -1;
|
||||||
|
|
||||||
|
// Check if this part contains array indexing
|
||||||
|
if (part.Contains("["))
|
||||||
|
{
|
||||||
|
int startBracket = part.IndexOf('[');
|
||||||
|
int endBracket = part.IndexOf(']');
|
||||||
|
if (startBracket > 0 && endBracket > startBracket)
|
||||||
|
{
|
||||||
|
string indexStr = part.Substring(startBracket + 1, endBracket - startBracket - 1);
|
||||||
|
if (int.TryParse(indexStr, out arrayIndex))
|
||||||
|
{
|
||||||
|
isArray = true;
|
||||||
|
part = part.Substring(0, startBracket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the property/field
|
||||||
|
PropertyInfo propInfo = currentType.GetProperty(part, flags);
|
||||||
|
FieldInfo fieldInfo = null;
|
||||||
|
if (propInfo == null)
|
||||||
|
{
|
||||||
|
fieldInfo = currentType.GetField(part, flags);
|
||||||
|
if (fieldInfo == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[SetNestedProperty] Could not find property or field '{part}' on type '{currentType.Name}'");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the value
|
||||||
|
currentObject = propInfo != null ? propInfo.GetValue(currentObject) : fieldInfo.GetValue(currentObject);
|
||||||
|
|
||||||
|
// If the current property is null, we need to stop
|
||||||
|
if (currentObject == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[SetNestedProperty] Property '{part}' is null, cannot access nested properties.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is an array/list access, get the element at the index
|
||||||
|
if (isArray)
|
||||||
|
{
|
||||||
|
if (currentObject is Material[])
|
||||||
|
{
|
||||||
|
var materials = currentObject as Material[];
|
||||||
|
if (arrayIndex < 0 || arrayIndex >= materials.Length)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[SetNestedProperty] Material index {arrayIndex} out of range (0-{materials.Length-1})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
currentObject = materials[arrayIndex];
|
||||||
|
}
|
||||||
|
else if (currentObject is System.Collections.IList)
|
||||||
|
{
|
||||||
|
var list = currentObject as System.Collections.IList;
|
||||||
|
if (arrayIndex < 0 || arrayIndex >= list.Count)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[SetNestedProperty] Index {arrayIndex} out of range (0-{list.Count-1})");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
currentObject = list[arrayIndex];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[SetNestedProperty] Property '{part}' is not an array or list, cannot access by index.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update type for next iteration
|
||||||
|
currentType = currentObject.GetType();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the final property
|
||||||
|
string finalPart = pathParts[pathParts.Length - 1];
|
||||||
|
|
||||||
|
// Special handling for Material properties (shader properties)
|
||||||
|
if (currentObject is Material material && finalPart.StartsWith("_"))
|
||||||
|
{
|
||||||
|
// Handle various material property types
|
||||||
|
if (value is JArray jArray)
|
||||||
|
{
|
||||||
|
if (jArray.Count == 4) // Color with alpha
|
||||||
|
{
|
||||||
|
Color color = new Color(
|
||||||
|
jArray[0].ToObject<float>(),
|
||||||
|
jArray[1].ToObject<float>(),
|
||||||
|
jArray[2].ToObject<float>(),
|
||||||
|
jArray[3].ToObject<float>()
|
||||||
|
);
|
||||||
|
material.SetColor(finalPart, color);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (jArray.Count == 3) // Color without alpha
|
||||||
|
{
|
||||||
|
Color color = new Color(
|
||||||
|
jArray[0].ToObject<float>(),
|
||||||
|
jArray[1].ToObject<float>(),
|
||||||
|
jArray[2].ToObject<float>(),
|
||||||
|
1.0f
|
||||||
|
);
|
||||||
|
material.SetColor(finalPart, color);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (jArray.Count == 2) // Vector2
|
||||||
|
{
|
||||||
|
Vector2 vec = new Vector2(
|
||||||
|
jArray[0].ToObject<float>(),
|
||||||
|
jArray[1].ToObject<float>()
|
||||||
|
);
|
||||||
|
material.SetVector(finalPart, vec);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (jArray.Count == 4) // Vector4
|
||||||
|
{
|
||||||
|
Vector4 vec = new Vector4(
|
||||||
|
jArray[0].ToObject<float>(),
|
||||||
|
jArray[1].ToObject<float>(),
|
||||||
|
jArray[2].ToObject<float>(),
|
||||||
|
jArray[3].ToObject<float>()
|
||||||
|
);
|
||||||
|
material.SetVector(finalPart, vec);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer)
|
||||||
|
{
|
||||||
|
material.SetFloat(finalPart, value.ToObject<float>());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (value.Type == JTokenType.Boolean)
|
||||||
|
{
|
||||||
|
material.SetFloat(finalPart, value.ToObject<bool>() ? 1f : 0f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (value.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
// Might be a texture path
|
||||||
|
string texturePath = value.ToString();
|
||||||
|
if (texturePath.EndsWith(".png") || texturePath.EndsWith(".jpg") || texturePath.EndsWith(".tga"))
|
||||||
|
{
|
||||||
|
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
|
||||||
|
if (texture != null)
|
||||||
|
{
|
||||||
|
material.SetTexture(finalPart, texture);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Materials don't have SetString, use SetTextureOffset as workaround or skip
|
||||||
|
// material.SetString(finalPart, texturePath);
|
||||||
|
Debug.LogWarning($"[SetNestedProperty] String values not directly supported for material property {finalPart}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.LogWarning($"[SetNestedProperty] Unsupported material property value type: {value.Type} for {finalPart}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For standard properties (not shader specific)
|
||||||
|
PropertyInfo finalPropInfo = currentType.GetProperty(finalPart, flags);
|
||||||
|
if (finalPropInfo != null && finalPropInfo.CanWrite)
|
||||||
|
{
|
||||||
|
object convertedValue = ConvertJTokenToType(value, finalPropInfo.PropertyType);
|
||||||
|
if (convertedValue != null)
|
||||||
|
{
|
||||||
|
finalPropInfo.SetValue(currentObject, convertedValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags);
|
||||||
|
if (finalFieldInfo != null)
|
||||||
|
{
|
||||||
|
object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType);
|
||||||
|
if (convertedValue != null)
|
||||||
|
{
|
||||||
|
finalFieldInfo.SetValue(currentObject, convertedValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[SetNestedProperty] Could not find final property or field '{finalPart}' on type '{currentType.Name}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[SetNestedProperty] Error setting nested property '{path}': {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Split a property path into parts, handling both dot notation and array indexers
|
||||||
|
/// </summary>
|
||||||
|
private static string[] SplitPropertyPath(string path)
|
||||||
|
{
|
||||||
|
// Handle complex paths with both dots and array indexers
|
||||||
|
List<string> parts = new List<string>();
|
||||||
|
int startIndex = 0;
|
||||||
|
bool inBrackets = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < path.Length; i++)
|
||||||
|
{
|
||||||
|
char c = path[i];
|
||||||
|
|
||||||
|
if (c == '[')
|
||||||
|
{
|
||||||
|
inBrackets = true;
|
||||||
|
}
|
||||||
|
else if (c == ']')
|
||||||
|
{
|
||||||
|
inBrackets = false;
|
||||||
|
}
|
||||||
|
else if (c == '.' && !inBrackets)
|
||||||
|
{
|
||||||
|
// Found a dot separator outside of brackets
|
||||||
|
parts.Add(path.Substring(startIndex, i - startIndex));
|
||||||
|
startIndex = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the final part
|
||||||
|
if (startIndex < path.Length)
|
||||||
|
{
|
||||||
|
parts.Add(path.Substring(startIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Simple JToken to Type conversion for common Unity types.
|
/// Simple JToken to Type conversion for common Unity types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1141,6 +1425,38 @@ namespace UnityMCP.Editor.Tools
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Unwrap nested material properties if we're assigning to a Material
|
||||||
|
if (typeof(Material).IsAssignableFrom(targetType) && token is JObject materialProps)
|
||||||
|
{
|
||||||
|
// Handle case where we're passing shader properties directly in a nested object
|
||||||
|
string materialPath = token["path"]?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(materialPath))
|
||||||
|
{
|
||||||
|
// Load the material by path
|
||||||
|
Material material = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
|
||||||
|
if (material != null)
|
||||||
|
{
|
||||||
|
// If there are additional properties, set them
|
||||||
|
foreach (var prop in materialProps.Properties())
|
||||||
|
{
|
||||||
|
if (prop.Name != "path")
|
||||||
|
{
|
||||||
|
SetProperty(material, prop.Name, prop.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[ConvertJTokenToType] Could not load material at path: '{materialPath}'");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no path is specified, could be a dynamic material or instance set by reference
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Basic types first
|
// Basic types first
|
||||||
if (targetType == typeof(string)) return token.ToObject<string>();
|
if (targetType == typeof(string)) return token.ToObject<string>();
|
||||||
if (targetType == typeof(int)) return token.ToObject<int>();
|
if (targetType == typeof(int)) return token.ToObject<int>();
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,26 @@ namespace UnityMCP.Editor.Tools
|
||||||
string action = @params["action"]?.ToString().ToLower();
|
string action = @params["action"]?.ToString().ToLower();
|
||||||
string name = @params["name"]?.ToString();
|
string name = @params["name"]?.ToString();
|
||||||
string path = @params["path"]?.ToString(); // Relative to Assets/
|
string path = @params["path"]?.ToString(); // Relative to Assets/
|
||||||
string contents = @params["contents"]?.ToString();
|
string contents = null;
|
||||||
|
|
||||||
|
// Check if we have base64 encoded contents
|
||||||
|
bool contentsEncoded = @params["contentsEncoded"]?.ToObject<bool>() ?? false;
|
||||||
|
if (contentsEncoded && @params["encodedContents"] != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
contents = DecodeBase64(@params["encodedContents"].ToString());
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Response.Error($"Failed to decode script contents: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
contents = @params["contents"]?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
string scriptType = @params["scriptType"]?.ToString(); // For templates/validation
|
string scriptType = @params["scriptType"]?.ToString(); // For templates/validation
|
||||||
string namespaceName = @params["namespace"]?.ToString(); // For organizing code
|
string namespaceName = @params["namespace"]?.ToString(); // For organizing code
|
||||||
|
|
||||||
|
|
@ -93,6 +112,24 @@ namespace UnityMCP.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decode base64 string to normal text
|
||||||
|
/// </summary>
|
||||||
|
private static string DecodeBase64(string encoded)
|
||||||
|
{
|
||||||
|
byte[] data = Convert.FromBase64String(encoded);
|
||||||
|
return System.Text.Encoding.UTF8.GetString(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encode text to base64 string
|
||||||
|
/// </summary>
|
||||||
|
private static string EncodeBase64(string text)
|
||||||
|
{
|
||||||
|
byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
|
||||||
|
return Convert.ToBase64String(data);
|
||||||
|
}
|
||||||
|
|
||||||
private static object CreateScript(string fullPath, string relativePath, string name, string contents, string scriptType, string namespaceName)
|
private static object CreateScript(string fullPath, string relativePath, string name, string contents, string scriptType, string namespaceName)
|
||||||
{
|
{
|
||||||
// Check if script already exists
|
// Check if script already exists
|
||||||
|
|
@ -138,7 +175,18 @@ namespace UnityMCP.Editor.Tools
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string contents = File.ReadAllText(fullPath);
|
string contents = File.ReadAllText(fullPath);
|
||||||
return Response.Success($"Script '{Path.GetFileName(relativePath)}' read successfully.", new { path = relativePath, contents = contents });
|
|
||||||
|
// Return both normal and encoded contents for larger files
|
||||||
|
bool isLarge = contents.Length > 10000; // If content is large, include encoded version
|
||||||
|
var responseData = new {
|
||||||
|
path = relativePath,
|
||||||
|
contents = contents,
|
||||||
|
// For large files, also include base64-encoded version
|
||||||
|
encodedContents = isLarge ? EncodeBase64(contents) : null,
|
||||||
|
contentsEncoded = isLarge
|
||||||
|
};
|
||||||
|
|
||||||
|
return Response.Success($"Script '{Path.GetFileName(relativePath)}' read successfully.", responseData);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
"""
|
"""
|
||||||
Defines the execute_menu_item tool for running Unity Editor menu commands.
|
Defines the execute_menu_item tool for running Unity Editor menu commands.
|
||||||
"""
|
"""
|
||||||
from typing import Optional, Dict, Any
|
from typing import Dict, Any
|
||||||
from mcp.server.fastmcp import FastMCP, Context
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
|
from unity_connection import get_unity_connection # Import unity_connection module
|
||||||
|
|
||||||
def register_execute_menu_item_tools(mcp: FastMCP):
|
def register_execute_menu_item_tools(mcp: FastMCP):
|
||||||
"""Registers the execute_menu_item tool with the MCP server."""
|
"""Registers the execute_menu_item tool with the MCP server."""
|
||||||
|
|
@ -11,8 +12,8 @@ def register_execute_menu_item_tools(mcp: FastMCP):
|
||||||
async def execute_menu_item(
|
async def execute_menu_item(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
menu_path: str,
|
menu_path: str,
|
||||||
action: Optional[str] = 'execute',
|
action: str = 'execute',
|
||||||
parameters: Optional[Dict[str, Any]] = None,
|
parameters: Dict[str, Any] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Executes a Unity Editor menu item via its path (e.g., "File/Save Project").
|
"""Executes a Unity Editor menu item via its path (e.g., "File/Save Project").
|
||||||
|
|
||||||
|
|
@ -41,9 +42,10 @@ def register_execute_menu_item_tools(mcp: FastMCP):
|
||||||
if "parameters" not in params_dict:
|
if "parameters" not in params_dict:
|
||||||
params_dict["parameters"] = {} # Ensure parameters dict exists
|
params_dict["parameters"] = {} # Ensure parameters dict exists
|
||||||
|
|
||||||
# Forward the command to the Unity editor handler
|
# Get Unity connection and send the command
|
||||||
# The C# handler is the static method HandleCommand in the ExecuteMenuItem class.
|
# We use the unity_connection module to communicate with Unity
|
||||||
# We assume ctx.call is the correct way to invoke it via FastMCP.
|
unity_conn = get_unity_connection()
|
||||||
# Note: The exact target string might need adjustment based on FastMCP's specifics.
|
|
||||||
csharp_handler_target = "UnityMCP.Editor.Tools.ExecuteMenuItem.HandleCommand"
|
# Send command to the ExecuteMenuItem C# handler
|
||||||
return await ctx.call(csharp_handler_target, params_dict)
|
# The command type should match what the Unity side expects
|
||||||
|
return unity_conn.send_command("execute_menu_item", params_dict)
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
Defines the manage_asset tool for interacting with Unity assets.
|
Defines the manage_asset tool for interacting with Unity assets.
|
||||||
"""
|
"""
|
||||||
import asyncio # Added: Import asyncio for running sync code in async
|
import asyncio # Added: Import asyncio for running sync code in async
|
||||||
from typing import Optional, Dict, Any, List
|
from typing import Dict, Any
|
||||||
from mcp.server.fastmcp import FastMCP, Context
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
# from ..unity_connection import get_unity_connection # Original line that caused error
|
# from ..unity_connection import get_unity_connection # Original line that caused error
|
||||||
from unity_connection import get_unity_connection # Use absolute import relative to Python dir
|
from unity_connection import get_unity_connection # Use absolute import relative to Python dir
|
||||||
|
|
@ -15,15 +15,15 @@ def register_manage_asset_tools(mcp: FastMCP):
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
action: str,
|
action: str,
|
||||||
path: str,
|
path: str,
|
||||||
asset_type: Optional[str] = None,
|
asset_type: str = None,
|
||||||
properties: Optional[Dict[str, Any]] = None,
|
properties: Dict[str, Any] = None,
|
||||||
destination: Optional[str] = None,
|
destination: str = None,
|
||||||
generate_preview: Optional[bool] = False,
|
generate_preview: bool = False,
|
||||||
search_pattern: Optional[str] = None,
|
search_pattern: str = None,
|
||||||
filter_type: Optional[str] = None,
|
filter_type: str = None,
|
||||||
filter_date_after: Optional[str] = None,
|
filter_date_after: str = None,
|
||||||
page_size: Optional[int] = None,
|
page_size: int = None,
|
||||||
page_number: Optional[int] = None
|
page_number: int = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Performs asset operations (import, create, modify, delete, etc.) in Unity.
|
"""Performs asset operations (import, create, modify, delete, etc.) in Unity.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from mcp.server.fastmcp import FastMCP, Context
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
from typing import Optional, Dict, Any, Union
|
from typing import Dict, Any
|
||||||
from unity_connection import get_unity_connection
|
from unity_connection import get_unity_connection
|
||||||
|
|
||||||
def register_manage_editor_tools(mcp: FastMCP):
|
def register_manage_editor_tools(mcp: FastMCP):
|
||||||
|
|
@ -9,11 +9,11 @@ def register_manage_editor_tools(mcp: FastMCP):
|
||||||
def manage_editor(
|
def manage_editor(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
action: str,
|
action: str,
|
||||||
wait_for_completion: Optional[bool] = None,
|
wait_for_completion: bool = None,
|
||||||
# --- Parameters for specific actions ---
|
# --- Parameters for specific actions ---
|
||||||
tool_name: Optional[str] = None,
|
tool_name: str = None,
|
||||||
tag_name: Optional[str] = None,
|
tag_name: str = None,
|
||||||
layer_name: Optional[str] = None,
|
layer_name: str = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Controls and queries the Unity editor's state and settings.
|
"""Controls and queries the Unity editor's state and settings.
|
||||||
|
|
||||||
|
|
@ -50,14 +50,4 @@ def register_manage_editor_tools(mcp: FastMCP):
|
||||||
return {"success": False, "message": response.get("error", "An unknown error occurred during editor management.")}
|
return {"success": False, "message": response.get("error", "An unknown error occurred during editor management.")}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"success": False, "message": f"Python error managing editor: {str(e)}"}
|
return {"success": False, "message": f"Python error managing editor: {str(e)}"}
|
||||||
|
|
||||||
# Example of potentially splitting into more specific tools:
|
|
||||||
# @mcp.tool()
|
|
||||||
# def get_editor_state(ctx: Context) -> Dict[str, Any]: ...
|
|
||||||
# @mcp.tool()
|
|
||||||
# def set_editor_playmode(ctx: Context, state: str) -> Dict[str, Any]: ... # state='play'/'pause'/'stop'
|
|
||||||
# @mcp.tool()
|
|
||||||
# def add_editor_tag(ctx: Context, tag_name: str) -> Dict[str, Any]: ...
|
|
||||||
# @mcp.tool()
|
|
||||||
# def add_editor_layer(ctx: Context, layer_name: str) -> Dict[str, Any]: ...
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from mcp.server.fastmcp import FastMCP, Context
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
from typing import Optional, Dict, Any, List, Union
|
from typing import Dict, Any, List
|
||||||
from unity_connection import get_unity_connection
|
from unity_connection import get_unity_connection
|
||||||
|
|
||||||
def register_manage_gameobject_tools(mcp: FastMCP):
|
def register_manage_gameobject_tools(mcp: FastMCP):
|
||||||
|
|
@ -9,42 +9,43 @@ def register_manage_gameobject_tools(mcp: FastMCP):
|
||||||
def manage_gameobject(
|
def manage_gameobject(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
action: str,
|
action: str,
|
||||||
target: Optional[Union[str, int]] = None,
|
target: str = None, # GameObject identifier by name or path
|
||||||
search_method: Optional[str] = None,
|
search_method: str = None,
|
||||||
# --- Parameters for 'create' ---
|
# --- Combined Parameters for Create/Modify ---
|
||||||
name: Optional[str] = None,
|
name: str = None, # Used for both 'create' (new object name) and 'modify' (rename)
|
||||||
tag: Optional[str] = None,
|
tag: str = None, # Used for both 'create' (initial tag) and 'modify' (change tag)
|
||||||
parent: Optional[Union[str, int]] = None,
|
parent: str = None, # Used for both 'create' (initial parent) and 'modify' (change parent)
|
||||||
position: Optional[List[float]] = None,
|
position: List[float] = None,
|
||||||
rotation: Optional[List[float]] = None,
|
rotation: List[float] = None,
|
||||||
scale: Optional[List[float]] = None,
|
scale: List[float] = None,
|
||||||
components_to_add: Optional[List[Union[str, Dict[str, Any]]]] = None,
|
components_to_add: List[str] = None, # List of component names to add
|
||||||
primitive_type: Optional[str] = None,
|
primitive_type: str = None,
|
||||||
save_as_prefab: Optional[bool] = False,
|
save_as_prefab: bool = False,
|
||||||
prefab_path: Optional[str] = None,
|
prefab_path: str = None,
|
||||||
prefab_folder: Optional[str] = "Assets/Prefabs",
|
prefab_folder: str = "Assets/Prefabs",
|
||||||
# --- Parameters for 'modify' ---
|
# --- Parameters for 'modify' ---
|
||||||
new_name: Optional[str] = None,
|
set_active: bool = None,
|
||||||
new_parent: Optional[Union[str, int]] = None,
|
layer: str = None, # Layer name
|
||||||
set_active: Optional[bool] = None,
|
components_to_remove: List[str] = None,
|
||||||
new_tag: Optional[str] = None,
|
component_properties: Dict[str, Dict[str, Any]] = None,
|
||||||
new_layer: Optional[Union[str, int]] = None,
|
|
||||||
components_to_remove: Optional[List[str]] = None,
|
|
||||||
component_properties: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
||||||
# --- Parameters for 'find' ---
|
# --- Parameters for 'find' ---
|
||||||
search_term: Optional[str] = None,
|
search_term: str = None,
|
||||||
find_all: Optional[bool] = False,
|
find_all: bool = False,
|
||||||
search_in_children: Optional[bool] = False,
|
search_in_children: bool = False,
|
||||||
search_inactive: Optional[bool] = False,
|
search_inactive: bool = False,
|
||||||
# -- Component Management Arguments --
|
# -- Component Management Arguments --
|
||||||
component_name: Optional[str] = None,
|
component_name: str = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Manages GameObjects: create, modify, delete, find, and component operations.
|
"""Manages GameObjects: create, modify, delete, find, and component operations.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property').
|
action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property').
|
||||||
target: GameObject identifier (name, path, ID) for modify/delete/component actions.
|
target: GameObject identifier (name or path string) for modify/delete/component actions.
|
||||||
search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find' and some 'target' lookups.
|
search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find' and some 'target' lookups.
|
||||||
|
name: GameObject name - used for both 'create' (initial name) and 'modify' (rename).
|
||||||
|
tag: Tag name - used for both 'create' (initial tag) and 'modify' (change tag).
|
||||||
|
parent: Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent).
|
||||||
|
layer: Layer name - used for both 'create' (initial layer) and 'modify' (change layer).
|
||||||
component_properties: Dict mapping Component names to their properties to set.
|
component_properties: Dict mapping Component names to their properties to set.
|
||||||
Example: {"Rigidbody": {"mass": 10.0, "useGravity": True}},
|
Example: {"Rigidbody": {"mass": 10.0, "useGravity": True}},
|
||||||
To set references:
|
To set references:
|
||||||
|
|
@ -52,7 +53,10 @@ def register_manage_gameobject_tools(mcp: FastMCP):
|
||||||
- Use a dict for scene objects/components, e.g.:
|
- Use a dict for scene objects/components, e.g.:
|
||||||
{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}} (assigns GameObject)
|
{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}} (assigns GameObject)
|
||||||
{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}} (assigns Component)
|
{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}} (assigns Component)
|
||||||
Action-specific arguments (e.g., name, parent, position for 'create';
|
Example set nested property:
|
||||||
|
- Access shared material: {"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}}
|
||||||
|
components_to_add: List of component names to add.
|
||||||
|
Action-specific arguments (e.g., position, rotation, scale for create/modify;
|
||||||
component_name for component actions;
|
component_name for component actions;
|
||||||
search_term, find_all for 'find').
|
search_term, find_all for 'find').
|
||||||
|
|
||||||
|
|
@ -79,11 +83,8 @@ def register_manage_gameobject_tools(mcp: FastMCP):
|
||||||
"saveAsPrefab": save_as_prefab,
|
"saveAsPrefab": save_as_prefab,
|
||||||
"prefabPath": prefab_path,
|
"prefabPath": prefab_path,
|
||||||
"prefabFolder": prefab_folder,
|
"prefabFolder": prefab_folder,
|
||||||
"newName": new_name,
|
|
||||||
"newParent": new_parent,
|
|
||||||
"setActive": set_active,
|
"setActive": set_active,
|
||||||
"newTag": new_tag,
|
"layer": layer,
|
||||||
"newLayer": new_layer,
|
|
||||||
"componentsToRemove": components_to_remove,
|
"componentsToRemove": components_to_remove,
|
||||||
"componentProperties": component_properties,
|
"componentProperties": component_properties,
|
||||||
"searchTerm": search_term,
|
"searchTerm": search_term,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from mcp.server.fastmcp import FastMCP, Context
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
from typing import Optional, Dict, Any
|
from typing import Dict, Any
|
||||||
from unity_connection import get_unity_connection
|
from unity_connection import get_unity_connection
|
||||||
|
|
||||||
def register_manage_scene_tools(mcp: FastMCP):
|
def register_manage_scene_tools(mcp: FastMCP):
|
||||||
|
|
@ -9,9 +9,9 @@ def register_manage_scene_tools(mcp: FastMCP):
|
||||||
def manage_scene(
|
def manage_scene(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
action: str,
|
action: str,
|
||||||
name: Optional[str] = None,
|
name: str,
|
||||||
path: Optional[str] = None,
|
path: str,
|
||||||
build_index: Optional[int] = None,
|
build_index: int,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Manages Unity scenes (load, save, create, get hierarchy, etc.).
|
"""Manages Unity scenes (load, save, create, get hierarchy, etc.).
|
||||||
|
|
||||||
|
|
@ -26,7 +26,6 @@ def register_manage_scene_tools(mcp: FastMCP):
|
||||||
Dictionary with results ('success', 'message', 'data').
|
Dictionary with results ('success', 'message', 'data').
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Prepare parameters, removing None values
|
|
||||||
params = {
|
params = {
|
||||||
"action": action,
|
"action": action,
|
||||||
"name": name,
|
"name": name,
|
||||||
|
|
@ -45,10 +44,4 @@ def register_manage_scene_tools(mcp: FastMCP):
|
||||||
return {"success": False, "message": response.get("error", "An unknown error occurred during scene management.")}
|
return {"success": False, "message": response.get("error", "An unknown error occurred during scene management.")}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"success": False, "message": f"Python error managing scene: {str(e)}"}
|
return {"success": False, "message": f"Python error managing scene: {str(e)}"}
|
||||||
|
|
||||||
# Consider adding specific tools if the single 'manage_scene' becomes too complex:
|
|
||||||
# @mcp.tool()
|
|
||||||
# def load_scene(ctx: Context, name: str, path: Optional[str] = None, build_index: Optional[int] = None) -> Dict[str, Any]: ...
|
|
||||||
# @mcp.tool()
|
|
||||||
# def get_scene_hierarchy(ctx: Context) -> Dict[str, Any]: ...
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
from mcp.server.fastmcp import FastMCP, Context
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
from typing import Optional, Dict, Any
|
from typing import Dict, Any
|
||||||
from unity_connection import get_unity_connection
|
from unity_connection import get_unity_connection
|
||||||
import os
|
import os
|
||||||
|
import base64
|
||||||
|
|
||||||
def register_manage_script_tools(mcp: FastMCP):
|
def register_manage_script_tools(mcp: FastMCP):
|
||||||
"""Register all script management tools with the MCP server."""
|
"""Register all script management tools with the MCP server."""
|
||||||
|
|
@ -11,10 +12,10 @@ def register_manage_script_tools(mcp: FastMCP):
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
action: str,
|
action: str,
|
||||||
name: str,
|
name: str,
|
||||||
path: Optional[str] = None,
|
path: str,
|
||||||
contents: Optional[str] = None,
|
contents: str,
|
||||||
script_type: Optional[str] = None,
|
script_type: str,
|
||||||
namespace: Optional[str] = None
|
namespace: str
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Manages C# scripts in Unity (create, read, update, delete).
|
"""Manages C# scripts in Unity (create, read, update, delete).
|
||||||
Make reference variables public for easier access in the Unity Editor.
|
Make reference variables public for easier access in the Unity Editor.
|
||||||
|
|
@ -22,10 +23,10 @@ def register_manage_script_tools(mcp: FastMCP):
|
||||||
Args:
|
Args:
|
||||||
action: Operation ('create', 'read', 'update', 'delete').
|
action: Operation ('create', 'read', 'update', 'delete').
|
||||||
name: Script name (no .cs extension).
|
name: Script name (no .cs extension).
|
||||||
path: Asset path (optional, default: "Assets/").
|
path: Asset path (default: "Assets/").
|
||||||
contents: C# code for 'create'/'update'.
|
contents: C# code for 'create'/'update'.
|
||||||
script_type: Type hint (e.g., 'MonoBehaviour', optional).
|
script_type: Type hint (e.g., 'MonoBehaviour').
|
||||||
namespace: Script namespace (optional).
|
namespace: Script namespace.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with results ('success', 'message', 'data').
|
Dictionary with results ('success', 'message', 'data').
|
||||||
|
|
@ -36,10 +37,19 @@ def register_manage_script_tools(mcp: FastMCP):
|
||||||
"action": action,
|
"action": action,
|
||||||
"name": name,
|
"name": name,
|
||||||
"path": path,
|
"path": path,
|
||||||
"contents": contents,
|
"namespace": namespace,
|
||||||
"scriptType": script_type,
|
"scriptType": script_type
|
||||||
"namespace": namespace
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Base64 encode the contents if they exist to avoid JSON escaping issues
|
||||||
|
if contents is not None:
|
||||||
|
if action in ['create', 'update']:
|
||||||
|
# Encode content for safer transmission
|
||||||
|
params["encodedContents"] = base64.b64encode(contents.encode('utf-8')).decode('utf-8')
|
||||||
|
params["contentsEncoded"] = True
|
||||||
|
else:
|
||||||
|
params["contents"] = contents
|
||||||
|
|
||||||
# Remove None values so they don't get sent as null
|
# Remove None values so they don't get sent as null
|
||||||
params = {k: v for k, v in params.items() if v is not None}
|
params = {k: v for k, v in params.items() if v is not None}
|
||||||
|
|
||||||
|
|
@ -48,17 +58,17 @@ def register_manage_script_tools(mcp: FastMCP):
|
||||||
|
|
||||||
# Process response from Unity
|
# Process response from Unity
|
||||||
if response.get("success"):
|
if response.get("success"):
|
||||||
|
# If the response contains base64 encoded content, decode it
|
||||||
|
if response.get("data", {}).get("contentsEncoded"):
|
||||||
|
decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8')
|
||||||
|
response["data"]["contents"] = decoded_contents
|
||||||
|
del response["data"]["encodedContents"]
|
||||||
|
del response["data"]["contentsEncoded"]
|
||||||
|
|
||||||
return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")}
|
return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")}
|
||||||
else:
|
else:
|
||||||
return {"success": False, "message": response.get("error", "An unknown error occurred.")}
|
return {"success": False, "message": response.get("error", "An unknown error occurred.")}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle Python-side errors (e.g., connection issues)
|
# Handle Python-side errors (e.g., connection issues)
|
||||||
return {"success": False, "message": f"Python error managing script: {str(e)}"}
|
return {"success": False, "message": f"Python error managing script: {str(e)}"}
|
||||||
|
|
||||||
# Potentially add more specific helper tools if needed later, e.g.:
|
|
||||||
# @mcp.tool()
|
|
||||||
# def create_script(...): ...
|
|
||||||
# @mcp.tool()
|
|
||||||
# def read_script(...): ...
|
|
||||||
# etc.
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"""
|
"""
|
||||||
Defines the read_console tool for accessing Unity Editor console messages.
|
Defines the read_console tool for accessing Unity Editor console messages.
|
||||||
"""
|
"""
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
from mcp.server.fastmcp import FastMCP, Context
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
from unity_connection import get_unity_connection
|
from unity_connection import get_unity_connection
|
||||||
|
|
||||||
def register_read_console_tools(mcp: FastMCP):
|
def register_read_console_tools(mcp: FastMCP):
|
||||||
"""Registers the read_console tool with the MCP server."""
|
"""Registers the read_console tool with the MCP server."""
|
||||||
|
|
@ -11,17 +11,18 @@ def register_read_console_tools(mcp: FastMCP):
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def read_console(
|
def read_console(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
action: Optional[str] = 'get',
|
action: str = None,
|
||||||
types: Optional[List[str]] = ['error', 'warning', 'log'],
|
types: List[str] = None,
|
||||||
count: Optional[int] = None,
|
count: int = None,
|
||||||
filter_text: Optional[str] = None,
|
filter_text: str = None,
|
||||||
since_timestamp: Optional[str] = None,
|
since_timestamp: str = None,
|
||||||
format: Optional[str] = 'detailed',
|
format: str = None,
|
||||||
include_stacktrace: Optional[bool] = True,
|
include_stacktrace: bool = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Gets messages from or clears the Unity Editor console.
|
"""Gets messages from or clears the Unity Editor console.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
ctx: The MCP context.
|
||||||
action: Operation ('get' or 'clear').
|
action: Operation ('get' or 'clear').
|
||||||
types: Message types to get ('error', 'warning', 'log', 'all').
|
types: Message types to get ('error', 'warning', 'log', 'all').
|
||||||
count: Max messages to return.
|
count: Max messages to return.
|
||||||
|
|
@ -37,17 +38,24 @@ def register_read_console_tools(mcp: FastMCP):
|
||||||
# Get the connection instance
|
# Get the connection instance
|
||||||
bridge = get_unity_connection()
|
bridge = get_unity_connection()
|
||||||
|
|
||||||
# Normalize action
|
# Set defaults if values are None
|
||||||
action = action.lower() if action else 'get'
|
action = action if action is not None else 'get'
|
||||||
|
types = types if types is not None else ['error', 'warning', 'log']
|
||||||
|
format = format if format is not None else 'detailed'
|
||||||
|
include_stacktrace = include_stacktrace if include_stacktrace is not None else True
|
||||||
|
|
||||||
|
# Normalize action if it's a string
|
||||||
|
if isinstance(action, str):
|
||||||
|
action = action.lower()
|
||||||
|
|
||||||
# Prepare parameters for the C# handler
|
# Prepare parameters for the C# handler
|
||||||
params_dict = {
|
params_dict = {
|
||||||
"action": action,
|
"action": action,
|
||||||
"types": types if types else ['error', 'warning', 'log'], # Ensure types is not None
|
"types": types,
|
||||||
"count": count,
|
"count": count,
|
||||||
"filterText": filter_text,
|
"filterText": filter_text,
|
||||||
"sinceTimestamp": since_timestamp,
|
"sinceTimestamp": since_timestamp,
|
||||||
"format": format.lower() if format else 'detailed',
|
"format": format.lower() if isinstance(format, str) else format,
|
||||||
"includeStacktrace": include_stacktrace
|
"includeStacktrace": include_stacktrace
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,6 +67,4 @@ def register_read_console_tools(mcp: FastMCP):
|
||||||
params_dict['count'] = None
|
params_dict['count'] = None
|
||||||
|
|
||||||
# Forward the command using the bridge's send_command method
|
# Forward the command using the bridge's send_command method
|
||||||
# The command type is the name of the tool itself in this case
|
|
||||||
# No await needed as send_command is synchronous
|
|
||||||
return bridge.send_command("read_console", params_dict)
|
return bridge.send_command("read_console", params_dict)
|
||||||
|
|
@ -125,10 +125,27 @@ class UnityConnection:
|
||||||
# Normal command handling
|
# Normal command handling
|
||||||
command = {"type": command_type, "params": params or {}}
|
command = {"type": command_type, "params": params or {}}
|
||||||
try:
|
try:
|
||||||
logger.info(f"Sending command: {command_type} with params: {params}")
|
# Check for very large content that might cause JSON issues
|
||||||
self.sock.sendall(json.dumps(command).encode('utf-8'))
|
command_size = len(json.dumps(command))
|
||||||
|
|
||||||
|
if command_size > config.buffer_size / 2:
|
||||||
|
logger.warning(f"Large command detected ({command_size} bytes). This might cause issues.")
|
||||||
|
|
||||||
|
logger.info(f"Sending command: {command_type} with params size: {command_size} bytes")
|
||||||
|
|
||||||
|
# Ensure we have a valid JSON string before sending
|
||||||
|
command_json = json.dumps(command, ensure_ascii=False)
|
||||||
|
self.sock.sendall(command_json.encode('utf-8'))
|
||||||
|
|
||||||
response_data = self.receive_full_response(self.sock)
|
response_data = self.receive_full_response(self.sock)
|
||||||
response = json.loads(response_data.decode('utf-8'))
|
try:
|
||||||
|
response = json.loads(response_data.decode('utf-8'))
|
||||||
|
except json.JSONDecodeError as je:
|
||||||
|
logger.error(f"JSON decode error: {str(je)}")
|
||||||
|
# Log partial response for debugging
|
||||||
|
partial_response = response_data.decode('utf-8')[:500] + "..." if len(response_data) > 500 else response_data.decode('utf-8')
|
||||||
|
logger.error(f"Partial response: {partial_response}")
|
||||||
|
raise Exception(f"Invalid JSON response from Unity: {str(je)}")
|
||||||
|
|
||||||
if response.get("status") == "error":
|
if response.get("status") == "error":
|
||||||
error_message = response.get("error") or response.get("message", "Unknown Unity error")
|
error_message = response.get("error") or response.get("message", "Unknown Unity error")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
Follow this detailed step-by-step guide to build this **"Crystal Climber"** game.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 1: Set Up the Basic Scene
|
||||||
|
1. Create a new 3D project named "Crystal Climber."
|
||||||
|
2. Add a large flat plane as the starting ground (this can act as the base of the climb).
|
||||||
|
3. Add a simple 3D cube or capsule as the player character.
|
||||||
|
4. Position the player on the ground plane, slightly above it (to account for gravity).
|
||||||
|
5. Add a directional light to illuminate the scene evenly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: Player Movement Basics
|
||||||
|
6. Implement basic WASD movement for the player (forward, backward, left, right).
|
||||||
|
7. Add a jump ability triggered by the spacebar.
|
||||||
|
8. Attach a third-person camera to follow the player (positioned slightly behind and above).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3: Build the Platform Structure
|
||||||
|
9. Create a flat, square platform (e.g., a thin cube or plane) as a prefab.
|
||||||
|
10. Place 5 platforms manually in the scene, staggered vertically and slightly offset horizontally (forming a climbable path upward).
|
||||||
|
11. Add collision to the platforms so the player can land on them.
|
||||||
|
12. Test the player jumping from the ground plane to the first platform and up the sequence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4: Core Objective
|
||||||
|
13. Place a glowing cube or sphere at the topmost platform as the "crystal."
|
||||||
|
14. Make the crystal detectable so the game recognizes when the player reaches it.
|
||||||
|
15. Add a win condition (e.g., display "You Win!" text on screen when the player touches the crystal).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 5: Visual Polish
|
||||||
|
16. Apply a semi-transparent material to the platforms (e.g., light blue with a faint glow).
|
||||||
|
17. Add a pulsing effect to the platforms (e.g., slight scale increase/decrease or opacity shift).
|
||||||
|
18. Change the scene background to a starry skybox.
|
||||||
|
19. Add a particle effect (e.g., sparkles or glowing dots) around the crystal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 6: Refine the Platforms
|
||||||
|
20. Adjust the spacing between platforms to ensure jumps are challenging but possible.
|
||||||
|
21. Add 5 more platforms (total 10) to extend the climb vertically.
|
||||||
|
22. Place a small floating orb or decorative object on one platform as a visual detail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 7: Audio Enhancement
|
||||||
|
23. Add a looping ambient background sound (e.g., soft wind or ethereal hum).
|
||||||
|
24. Attach a jump sound to the player (e.g., a light tap or whoosh).
|
||||||
|
25. Add a short victory sound (e.g., a chime or jingle) when the player reaches the crystal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 8: Final Touches for Devlog Appeal
|
||||||
|
26. Add a subtle camera zoom-in effect when the player touches the crystal.
|
||||||
|
27. Sprinkle a few particle effects (e.g., faint stars or mist) across the scene for atmosphere.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Extras
|
||||||
|
29. Add a double-jump ability (e.g., press space twice) to make platforming easier.
|
||||||
|
30. Place a slow-rotating spike ball on one platform as a hazard to jump over.
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 59f0a16c19ac31d48a5b294600c96873
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Loading…
Reference in New Issue