remove all optional and union and any parameters for cursor

main
Justin Barnett 2025-03-31 16:34:24 -04:00
parent ba4c2a85bf
commit 0b51ff50d5
12 changed files with 608 additions and 152 deletions

View File

@ -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;
@ -309,6 +314,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>();

View File

@ -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)
{ {

View File

@ -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)

View File

@ -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.

View File

@ -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.
@ -51,13 +51,3 @@ def register_manage_editor_tools(mcp: FastMCP):
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]: ...

View File

@ -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,

View File

@ -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,
@ -46,9 +45,3 @@ def register_manage_scene_tools(mcp: FastMCP):
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]: ...

View File

@ -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,6 +58,13 @@ 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.")}
@ -55,10 +72,3 @@ def register_manage_script_tools(mcp: FastMCP):
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.

View File

@ -1,7 +1,7 @@
""" """
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
@ -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)

View File

@ -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")

66
climber-prompt.md Normal file
View File

@ -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.

7
climber-prompt.md.meta Normal file
View File

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