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
|
||||
JToken targetToken = @params["target"]; // Can be string (name/path) or int (instanceID)
|
||||
string searchMethod = @params["searchMethod"]?.ToString().ToLower();
|
||||
|
||||
// Get common parameters (consolidated)
|
||||
string name = @params["name"]?.ToString();
|
||||
string tag = @params["tag"]?.ToString();
|
||||
string layer = @params["layer"]?.ToString();
|
||||
JToken parentToken = @params["parent"];
|
||||
|
||||
// --- Prefab Redirection Check ---
|
||||
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
|
||||
if (@params["componentsToAdd"] is JArray componentsToAddArray)
|
||||
|
|
@ -438,22 +458,22 @@ namespace UnityMCP.Editor.Tools
|
|||
|
||||
bool modified = false;
|
||||
|
||||
// Rename
|
||||
string newName = @params["newName"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(newName) && targetGo.name != newName)
|
||||
// Rename (using consolidated 'name' parameter)
|
||||
string name = @params["name"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(name) && targetGo.name != name)
|
||||
{
|
||||
targetGo.name = newName;
|
||||
modified = true;
|
||||
targetGo.name = name;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Change Parent
|
||||
JToken newParentToken = @params["newParent"];
|
||||
if (newParentToken != null)
|
||||
// Change Parent (using consolidated 'parent' parameter)
|
||||
JToken parentToken = @params["parent"];
|
||||
if (parentToken != null)
|
||||
{
|
||||
GameObject newParentGo = FindObjectInternal(newParentToken, "by_id_or_name_or_path");
|
||||
if (newParentGo == null && !(newParentToken.Type == JTokenType.Null || (newParentToken.Type == JTokenType.String && string.IsNullOrEmpty(newParentToken.ToString()))))
|
||||
GameObject newParentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path");
|
||||
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
|
||||
if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform))
|
||||
|
|
@ -475,14 +495,14 @@ namespace UnityMCP.Editor.Tools
|
|||
modified = true;
|
||||
}
|
||||
|
||||
// Change Tag
|
||||
string newTag = @params["newTag"]?.ToString();
|
||||
// Change Tag (using consolidated 'tag' parameter)
|
||||
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.
|
||||
// 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
|
||||
string tagToSet = string.IsNullOrEmpty(newTag) ? "Untagged" : newTag;
|
||||
string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag;
|
||||
|
||||
try {
|
||||
// First attempt to set the tag
|
||||
|
|
@ -522,28 +542,19 @@ namespace UnityMCP.Editor.Tools
|
|||
}
|
||||
}
|
||||
|
||||
// Change Layer
|
||||
JToken newLayerToken = @params["newLayer"];
|
||||
if (newLayerToken != null)
|
||||
// Change Layer (using consolidated 'layer' parameter)
|
||||
string layerName = @params["layer"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(layerName))
|
||||
{
|
||||
int layer = -1;
|
||||
if (newLayerToken.Type == JTokenType.Integer)
|
||||
int layerId = LayerMask.NameToLayer(layerName);
|
||||
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());
|
||||
}
|
||||
|
||||
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;
|
||||
targetGo.layer = layerId;
|
||||
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).");
|
||||
}
|
||||
|
||||
// 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
|
||||
if (properties != null)
|
||||
{
|
||||
|
|
@ -1104,6 +1122,13 @@ namespace UnityMCP.Editor.Tools
|
|||
|
||||
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);
|
||||
if (propInfo != null && propInfo.CanWrite)
|
||||
{
|
||||
|
|
@ -1134,6 +1159,265 @@ namespace UnityMCP.Editor.Tools
|
|||
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>
|
||||
/// Simple JToken to Type conversion for common Unity types.
|
||||
/// </summary>
|
||||
|
|
@ -1141,6 +1425,38 @@ namespace UnityMCP.Editor.Tools
|
|||
{
|
||||
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
|
||||
if (targetType == typeof(string)) return token.ToObject<string>();
|
||||
if (targetType == typeof(int)) return token.ToObject<int>();
|
||||
|
|
|
|||
|
|
@ -23,7 +23,26 @@ namespace UnityMCP.Editor.Tools
|
|||
string action = @params["action"]?.ToString().ToLower();
|
||||
string name = @params["name"]?.ToString();
|
||||
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 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)
|
||||
{
|
||||
// Check if script already exists
|
||||
|
|
@ -138,7 +175,18 @@ namespace UnityMCP.Editor.Tools
|
|||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
"""
|
||||
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 unity_connection import get_unity_connection # Import unity_connection module
|
||||
|
||||
def register_execute_menu_item_tools(mcp: FastMCP):
|
||||
"""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(
|
||||
ctx: Context,
|
||||
menu_path: str,
|
||||
action: Optional[str] = 'execute',
|
||||
parameters: Optional[Dict[str, Any]] = None,
|
||||
action: str = 'execute',
|
||||
parameters: Dict[str, Any] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""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:
|
||||
params_dict["parameters"] = {} # Ensure parameters dict exists
|
||||
|
||||
# Forward the command to the Unity editor handler
|
||||
# The C# handler is the static method HandleCommand in the ExecuteMenuItem class.
|
||||
# We assume ctx.call is the correct way to invoke it via FastMCP.
|
||||
# Note: The exact target string might need adjustment based on FastMCP's specifics.
|
||||
csharp_handler_target = "UnityMCP.Editor.Tools.ExecuteMenuItem.HandleCommand"
|
||||
return await ctx.call(csharp_handler_target, params_dict)
|
||||
# Get Unity connection and send the command
|
||||
# We use the unity_connection module to communicate with Unity
|
||||
unity_conn = get_unity_connection()
|
||||
|
||||
# Send command to the ExecuteMenuItem C# handler
|
||||
# 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.
|
||||
"""
|
||||
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 ..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
|
||||
|
|
@ -15,15 +15,15 @@ def register_manage_asset_tools(mcp: FastMCP):
|
|||
ctx: Context,
|
||||
action: str,
|
||||
path: str,
|
||||
asset_type: Optional[str] = None,
|
||||
properties: Optional[Dict[str, Any]] = None,
|
||||
destination: Optional[str] = None,
|
||||
generate_preview: Optional[bool] = False,
|
||||
search_pattern: Optional[str] = None,
|
||||
filter_type: Optional[str] = None,
|
||||
filter_date_after: Optional[str] = None,
|
||||
page_size: Optional[int] = None,
|
||||
page_number: Optional[int] = None
|
||||
asset_type: str = None,
|
||||
properties: Dict[str, Any] = None,
|
||||
destination: str = None,
|
||||
generate_preview: bool = False,
|
||||
search_pattern: str = None,
|
||||
filter_type: str = None,
|
||||
filter_date_after: str = None,
|
||||
page_size: int = None,
|
||||
page_number: int = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Performs asset operations (import, create, modify, delete, etc.) in Unity.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
||||
def register_manage_editor_tools(mcp: FastMCP):
|
||||
|
|
@ -9,11 +9,11 @@ def register_manage_editor_tools(mcp: FastMCP):
|
|||
def manage_editor(
|
||||
ctx: Context,
|
||||
action: str,
|
||||
wait_for_completion: Optional[bool] = None,
|
||||
wait_for_completion: bool = None,
|
||||
# --- Parameters for specific actions ---
|
||||
tool_name: Optional[str] = None,
|
||||
tag_name: Optional[str] = None,
|
||||
layer_name: Optional[str] = None,
|
||||
tool_name: str = None,
|
||||
tag_name: str = None,
|
||||
layer_name: str = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""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.")}
|
||||
|
||||
except Exception as 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]: ...
|
||||
return {"success": False, "message": f"Python error managing editor: {str(e)}"}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
||||
def register_manage_gameobject_tools(mcp: FastMCP):
|
||||
|
|
@ -9,42 +9,43 @@ def register_manage_gameobject_tools(mcp: FastMCP):
|
|||
def manage_gameobject(
|
||||
ctx: Context,
|
||||
action: str,
|
||||
target: Optional[Union[str, int]] = None,
|
||||
search_method: Optional[str] = None,
|
||||
# --- Parameters for 'create' ---
|
||||
name: Optional[str] = None,
|
||||
tag: Optional[str] = None,
|
||||
parent: Optional[Union[str, int]] = None,
|
||||
position: Optional[List[float]] = None,
|
||||
rotation: Optional[List[float]] = None,
|
||||
scale: Optional[List[float]] = None,
|
||||
components_to_add: Optional[List[Union[str, Dict[str, Any]]]] = None,
|
||||
primitive_type: Optional[str] = None,
|
||||
save_as_prefab: Optional[bool] = False,
|
||||
prefab_path: Optional[str] = None,
|
||||
prefab_folder: Optional[str] = "Assets/Prefabs",
|
||||
target: str = None, # GameObject identifier by name or path
|
||||
search_method: str = None,
|
||||
# --- Combined Parameters for Create/Modify ---
|
||||
name: str = None, # Used for both 'create' (new object name) and 'modify' (rename)
|
||||
tag: str = None, # Used for both 'create' (initial tag) and 'modify' (change tag)
|
||||
parent: str = None, # Used for both 'create' (initial parent) and 'modify' (change parent)
|
||||
position: List[float] = None,
|
||||
rotation: List[float] = None,
|
||||
scale: List[float] = None,
|
||||
components_to_add: List[str] = None, # List of component names to add
|
||||
primitive_type: str = None,
|
||||
save_as_prefab: bool = False,
|
||||
prefab_path: str = None,
|
||||
prefab_folder: str = "Assets/Prefabs",
|
||||
# --- Parameters for 'modify' ---
|
||||
new_name: Optional[str] = None,
|
||||
new_parent: Optional[Union[str, int]] = None,
|
||||
set_active: Optional[bool] = None,
|
||||
new_tag: Optional[str] = None,
|
||||
new_layer: Optional[Union[str, int]] = None,
|
||||
components_to_remove: Optional[List[str]] = None,
|
||||
component_properties: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||
set_active: bool = None,
|
||||
layer: str = None, # Layer name
|
||||
components_to_remove: List[str] = None,
|
||||
component_properties: Dict[str, Dict[str, Any]] = None,
|
||||
# --- Parameters for 'find' ---
|
||||
search_term: Optional[str] = None,
|
||||
find_all: Optional[bool] = False,
|
||||
search_in_children: Optional[bool] = False,
|
||||
search_inactive: Optional[bool] = False,
|
||||
search_term: str = None,
|
||||
find_all: bool = False,
|
||||
search_in_children: bool = False,
|
||||
search_inactive: bool = False,
|
||||
# -- Component Management Arguments --
|
||||
component_name: Optional[str] = None,
|
||||
component_name: str = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Manages GameObjects: create, modify, delete, find, and component operations.
|
||||
|
||||
Args:
|
||||
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.
|
||||
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.
|
||||
Example: {"Rigidbody": {"mass": 10.0, "useGravity": True}},
|
||||
To set references:
|
||||
|
|
@ -52,7 +53,10 @@ def register_manage_gameobject_tools(mcp: FastMCP):
|
|||
- Use a dict for scene objects/components, e.g.:
|
||||
{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}} (assigns GameObject)
|
||||
{"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;
|
||||
search_term, find_all for 'find').
|
||||
|
||||
|
|
@ -79,11 +83,8 @@ def register_manage_gameobject_tools(mcp: FastMCP):
|
|||
"saveAsPrefab": save_as_prefab,
|
||||
"prefabPath": prefab_path,
|
||||
"prefabFolder": prefab_folder,
|
||||
"newName": new_name,
|
||||
"newParent": new_parent,
|
||||
"setActive": set_active,
|
||||
"newTag": new_tag,
|
||||
"newLayer": new_layer,
|
||||
"layer": layer,
|
||||
"componentsToRemove": components_to_remove,
|
||||
"componentProperties": component_properties,
|
||||
"searchTerm": search_term,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
||||
def register_manage_scene_tools(mcp: FastMCP):
|
||||
|
|
@ -9,9 +9,9 @@ def register_manage_scene_tools(mcp: FastMCP):
|
|||
def manage_scene(
|
||||
ctx: Context,
|
||||
action: str,
|
||||
name: Optional[str] = None,
|
||||
path: Optional[str] = None,
|
||||
build_index: Optional[int] = None,
|
||||
name: str,
|
||||
path: str,
|
||||
build_index: int,
|
||||
) -> Dict[str, Any]:
|
||||
"""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').
|
||||
"""
|
||||
try:
|
||||
# Prepare parameters, removing None values
|
||||
params = {
|
||||
"action": action,
|
||||
"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.")}
|
||||
|
||||
except Exception as 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]: ...
|
||||
return {"success": False, "message": f"Python error managing scene: {str(e)}"}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
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
|
||||
import os
|
||||
import base64
|
||||
|
||||
def register_manage_script_tools(mcp: FastMCP):
|
||||
"""Register all script management tools with the MCP server."""
|
||||
|
|
@ -11,10 +12,10 @@ def register_manage_script_tools(mcp: FastMCP):
|
|||
ctx: Context,
|
||||
action: str,
|
||||
name: str,
|
||||
path: Optional[str] = None,
|
||||
contents: Optional[str] = None,
|
||||
script_type: Optional[str] = None,
|
||||
namespace: Optional[str] = None
|
||||
path: str,
|
||||
contents: str,
|
||||
script_type: str,
|
||||
namespace: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Manages C# scripts in Unity (create, read, update, delete).
|
||||
Make reference variables public for easier access in the Unity Editor.
|
||||
|
|
@ -22,10 +23,10 @@ def register_manage_script_tools(mcp: FastMCP):
|
|||
Args:
|
||||
action: Operation ('create', 'read', 'update', 'delete').
|
||||
name: Script name (no .cs extension).
|
||||
path: Asset path (optional, default: "Assets/").
|
||||
path: Asset path (default: "Assets/").
|
||||
contents: C# code for 'create'/'update'.
|
||||
script_type: Type hint (e.g., 'MonoBehaviour', optional).
|
||||
namespace: Script namespace (optional).
|
||||
script_type: Type hint (e.g., 'MonoBehaviour').
|
||||
namespace: Script namespace.
|
||||
|
||||
Returns:
|
||||
Dictionary with results ('success', 'message', 'data').
|
||||
|
|
@ -36,10 +37,19 @@ def register_manage_script_tools(mcp: FastMCP):
|
|||
"action": action,
|
||||
"name": name,
|
||||
"path": path,
|
||||
"contents": contents,
|
||||
"scriptType": script_type,
|
||||
"namespace": namespace
|
||||
"namespace": namespace,
|
||||
"scriptType": script_type
|
||||
}
|
||||
|
||||
# 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
|
||||
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
|
||||
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")}
|
||||
else:
|
||||
return {"success": False, "message": response.get("error", "An unknown error occurred.")}
|
||||
|
||||
except Exception as e:
|
||||
# Handle Python-side errors (e.g., connection issues)
|
||||
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.
|
||||
return {"success": False, "message": f"Python error managing script: {str(e)}"}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
"""
|
||||
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 unity_connection import get_unity_connection
|
||||
from unity_connection import get_unity_connection
|
||||
|
||||
def register_read_console_tools(mcp: FastMCP):
|
||||
"""Registers the read_console tool with the MCP server."""
|
||||
|
|
@ -11,17 +11,18 @@ def register_read_console_tools(mcp: FastMCP):
|
|||
@mcp.tool()
|
||||
def read_console(
|
||||
ctx: Context,
|
||||
action: Optional[str] = 'get',
|
||||
types: Optional[List[str]] = ['error', 'warning', 'log'],
|
||||
count: Optional[int] = None,
|
||||
filter_text: Optional[str] = None,
|
||||
since_timestamp: Optional[str] = None,
|
||||
format: Optional[str] = 'detailed',
|
||||
include_stacktrace: Optional[bool] = True,
|
||||
action: str = None,
|
||||
types: List[str] = None,
|
||||
count: int = None,
|
||||
filter_text: str = None,
|
||||
since_timestamp: str = None,
|
||||
format: str = None,
|
||||
include_stacktrace: bool = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Gets messages from or clears the Unity Editor console.
|
||||
|
||||
Args:
|
||||
ctx: The MCP context.
|
||||
action: Operation ('get' or 'clear').
|
||||
types: Message types to get ('error', 'warning', 'log', 'all').
|
||||
count: Max messages to return.
|
||||
|
|
@ -37,17 +38,24 @@ def register_read_console_tools(mcp: FastMCP):
|
|||
# Get the connection instance
|
||||
bridge = get_unity_connection()
|
||||
|
||||
# Normalize action
|
||||
action = action.lower() if action else 'get'
|
||||
# Set defaults if values are None
|
||||
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
|
||||
params_dict = {
|
||||
"action": action,
|
||||
"types": types if types else ['error', 'warning', 'log'], # Ensure types is not None
|
||||
"types": types,
|
||||
"count": count,
|
||||
"filterText": filter_text,
|
||||
"sinceTimestamp": since_timestamp,
|
||||
"format": format.lower() if format else 'detailed',
|
||||
"format": format.lower() if isinstance(format, str) else format,
|
||||
"includeStacktrace": include_stacktrace
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +67,4 @@ def register_read_console_tools(mcp: FastMCP):
|
|||
params_dict['count'] = None
|
||||
|
||||
# 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)
|
||||
|
|
@ -125,10 +125,27 @@ class UnityConnection:
|
|||
# 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'))
|
||||
# Check for very large content that might cause JSON issues
|
||||
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 = 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":
|
||||
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