Fix: Fix vector and color parameter validation to accept JSON string inputs (#625)
* fix: change vector/color parameter types to Any to allow string inputs * fix: add type check in manage_component to pass cursor error The Pydantic validation in FastMCP occurs before function execution, causing validation errors when clients pass string values like '[2, 2, 2]' for parameters typed as `list[float] | str`. Since the code already has normalization functions (_normalize_vector, _normalize_color) that handle string inputs, change the type annotations to `Any` to bypass Pydantic's strict validation. Affected parameters: - manage_gameobject: position, rotation, scale, offset - manage_material: color Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve vector parameter validation with clear error messages - Change parameter types from `list[float]` to `list[float] | str` to accept both list and JSON string inputs (consistent with read_console/run_tests) - Modify _normalize_vector to return (value, error_message) tuple instead of silently returning None on invalid input - Add detailed error messages for invalid vector values This fixes the Pydantic validation error when clients pass string values like "[2, 2, 2]" for scale/position/rotation parameters. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Loosen the type to pass the cursor error --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>main
parent
6650e72cdf
commit
4991d71eab
|
|
@ -37,7 +37,7 @@ async def manage_components(
|
||||||
# For set_property action - single property
|
# For set_property action - single property
|
||||||
property: Annotated[str,
|
property: Annotated[str,
|
||||||
"Property name to set (for set_property action)"] | None = None,
|
"Property name to set (for set_property action)"] | None = None,
|
||||||
value: Annotated[Any,
|
value: Annotated[str | int | float | bool | dict | list ,
|
||||||
"Value to set (for set_property action)"] | None = None,
|
"Value to set (for set_property action)"] | None = None,
|
||||||
# For add/set_property - multiple properties
|
# For add/set_property - multiple properties
|
||||||
properties: Annotated[
|
properties: Annotated[
|
||||||
|
|
|
||||||
|
|
@ -13,32 +13,40 @@ from services.tools.utils import coerce_bool, parse_json_payload, coerce_int
|
||||||
from services.tools.preflight import preflight
|
from services.tools.preflight import preflight
|
||||||
|
|
||||||
|
|
||||||
def _normalize_vector(value: Any, default: Any = None) -> list[float] | None:
|
def _normalize_vector(value: Any, param_name: str = "vector") -> tuple[list[float] | None, str | None]:
|
||||||
"""
|
"""
|
||||||
Robustly normalize a vector parameter to [x, y, z] format.
|
Robustly normalize a vector parameter to [x, y, z] format.
|
||||||
Handles: list, tuple, JSON string, comma-separated string.
|
Handles: list, tuple, JSON string, comma-separated string.
|
||||||
Returns None if parsing fails.
|
Returns (parsed_vector, error_message). If error_message is set, parsed_vector is None.
|
||||||
"""
|
"""
|
||||||
if value is None:
|
if value is None:
|
||||||
return default
|
return None, None
|
||||||
|
|
||||||
# If already a list/tuple with 3 elements, convert to floats
|
# If already a list/tuple with 3 elements, convert to floats
|
||||||
if isinstance(value, (list, tuple)) and len(value) == 3:
|
if isinstance(value, (list, tuple)) and len(value) == 3:
|
||||||
try:
|
try:
|
||||||
vec = [float(value[0]), float(value[1]), float(value[2])]
|
vec = [float(value[0]), float(value[1]), float(value[2])]
|
||||||
return vec if all(math.isfinite(n) for n in vec) else default
|
if all(math.isfinite(n) for n in vec):
|
||||||
|
return vec, None
|
||||||
|
return None, f"{param_name} values must be finite numbers, got {value}"
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return default
|
return None, f"{param_name} values must be numbers, got {value}"
|
||||||
|
|
||||||
# Try parsing as JSON string
|
# Try parsing as JSON string
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
|
# Check for obviously invalid values
|
||||||
|
if value in ("[object Object]", "undefined", "null", ""):
|
||||||
|
return None, f"{param_name} received invalid value: '{value}'. Expected [x, y, z] array (list or JSON string)"
|
||||||
|
|
||||||
parsed = parse_json_payload(value)
|
parsed = parse_json_payload(value)
|
||||||
if isinstance(parsed, list) and len(parsed) == 3:
|
if isinstance(parsed, list) and len(parsed) == 3:
|
||||||
try:
|
try:
|
||||||
vec = [float(parsed[0]), float(parsed[1]), float(parsed[2])]
|
vec = [float(parsed[0]), float(parsed[1]), float(parsed[2])]
|
||||||
return vec if all(math.isfinite(n) for n in vec) else default
|
if all(math.isfinite(n) for n in vec):
|
||||||
|
return vec, None
|
||||||
|
return None, f"{param_name} values must be finite numbers, got {parsed}"
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
return None, f"{param_name} values must be numbers, got {parsed}"
|
||||||
|
|
||||||
# Handle legacy comma-separated strings "1,2,3" or "[1,2,3]"
|
# Handle legacy comma-separated strings "1,2,3" or "[1,2,3]"
|
||||||
s = value.strip()
|
s = value.strip()
|
||||||
|
|
@ -48,11 +56,15 @@ def _normalize_vector(value: Any, default: Any = None) -> list[float] | None:
|
||||||
if len(parts) == 3:
|
if len(parts) == 3:
|
||||||
try:
|
try:
|
||||||
vec = [float(parts[0]), float(parts[1]), float(parts[2])]
|
vec = [float(parts[0]), float(parts[1]), float(parts[2])]
|
||||||
return vec if all(math.isfinite(n) for n in vec) else default
|
if all(math.isfinite(n) for n in vec):
|
||||||
|
return vec, None
|
||||||
|
return None, f"{param_name} values must be finite numbers, got {value}"
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
return None, f"{param_name} values must be numbers, got {value}"
|
||||||
|
|
||||||
return default
|
return None, f"{param_name} must be a [x, y, z] array (list or JSON string), got: {value}"
|
||||||
|
|
||||||
|
return None, f"{param_name} must be a list or JSON string, got {type(value).__name__}"
|
||||||
|
|
||||||
|
|
||||||
def _normalize_component_properties(value: Any) -> tuple[dict[str, dict[str, Any]] | None, str | None]:
|
def _normalize_component_properties(value: Any) -> tuple[dict[str, dict[str, Any]] | None, str | None]:
|
||||||
|
|
@ -103,12 +115,12 @@ async def manage_gameobject(
|
||||||
"Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None,
|
"Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None,
|
||||||
parent: Annotated[str,
|
parent: Annotated[str,
|
||||||
"Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | None = None,
|
"Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | None = None,
|
||||||
position: Annotated[list[float],
|
position: Annotated[list[float] | str,
|
||||||
"Position as [x, y, z] array"] | None = None,
|
"Position as [x, y, z] array (list or JSON string)"] | None = None,
|
||||||
rotation: Annotated[list[float],
|
rotation: Annotated[list[float] | str,
|
||||||
"Rotation as [x, y, z] euler angles array"] | None = None,
|
"Rotation as [x, y, z] euler angles array (list or JSON string)"] | None = None,
|
||||||
scale: Annotated[list[float],
|
scale: Annotated[list[float] | str,
|
||||||
"Scale as [x, y, z] array"] | None = None,
|
"Scale as [x, y, z] array (list or JSON string)"] | None = None,
|
||||||
components_to_add: Annotated[list[str],
|
components_to_add: Annotated[list[str],
|
||||||
"List of component names to add"] | None = None,
|
"List of component names to add"] | None = None,
|
||||||
primitive_type: Annotated[str,
|
primitive_type: Annotated[str,
|
||||||
|
|
@ -157,8 +169,8 @@ async def manage_gameobject(
|
||||||
# --- Parameters for 'duplicate' ---
|
# --- Parameters for 'duplicate' ---
|
||||||
new_name: Annotated[str,
|
new_name: Annotated[str,
|
||||||
"New name for the duplicated object (default: SourceName_Copy)"] | None = None,
|
"New name for the duplicated object (default: SourceName_Copy)"] | None = None,
|
||||||
offset: Annotated[list[float],
|
offset: Annotated[list[float] | str,
|
||||||
"Offset from original/reference position as [x, y, z] array"] | None = None,
|
"Offset from original/reference position as [x, y, z] array (list or JSON string)"] | None = None,
|
||||||
# --- Parameters for 'move_relative' ---
|
# --- Parameters for 'move_relative' ---
|
||||||
reference_object: Annotated[str,
|
reference_object: Annotated[str,
|
||||||
"Reference object for relative movement (required for move_relative)"] | None = None,
|
"Reference object for relative movement (required for move_relative)"] | None = None,
|
||||||
|
|
@ -183,11 +195,19 @@ async def manage_gameobject(
|
||||||
"message": "Missing required parameter 'action'. Valid actions: create, modify, delete, duplicate, move_relative. For finding GameObjects use find_gameobjects tool. For component operations use manage_components tool."
|
"message": "Missing required parameter 'action'. Valid actions: create, modify, delete, duplicate, move_relative. For finding GameObjects use find_gameobjects tool. For component operations use manage_components tool."
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Normalize vector parameters using robust helper ---
|
# --- Normalize vector parameters with detailed error handling ---
|
||||||
position = _normalize_vector(position)
|
position, position_error = _normalize_vector(position, "position")
|
||||||
rotation = _normalize_vector(rotation)
|
if position_error:
|
||||||
scale = _normalize_vector(scale)
|
return {"success": False, "message": position_error}
|
||||||
offset = _normalize_vector(offset)
|
rotation, rotation_error = _normalize_vector(rotation, "rotation")
|
||||||
|
if rotation_error:
|
||||||
|
return {"success": False, "message": rotation_error}
|
||||||
|
scale, scale_error = _normalize_vector(scale, "scale")
|
||||||
|
if scale_error:
|
||||||
|
return {"success": False, "message": scale_error}
|
||||||
|
offset, offset_error = _normalize_vector(offset, "offset")
|
||||||
|
if offset_error:
|
||||||
|
return {"success": False, "message": offset_error}
|
||||||
|
|
||||||
# --- Normalize boolean parameters ---
|
# --- Normalize boolean parameters ---
|
||||||
save_as_prefab = coerce_bool(save_as_prefab)
|
save_as_prefab = coerce_bool(save_as_prefab)
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,8 @@ async def manage_material(
|
||||||
"Value to set (color array, float, texture path/instruction)"] | None = None,
|
"Value to set (color array, float, texture path/instruction)"] | None = None,
|
||||||
|
|
||||||
# set_material_color / set_renderer_color
|
# set_material_color / set_renderer_color
|
||||||
color: Annotated[list[float],
|
color: Annotated[list[float] | str,
|
||||||
"Color as [r, g, b] or [r, g, b, a] array."] | None = None,
|
"Color as [r, g, b] or [r, g, b, a] array (list or JSON string)."] | None = None,
|
||||||
|
|
||||||
# assign_material_to_renderer / set_renderer_color
|
# assign_material_to_renderer / set_renderer_color
|
||||||
target: Annotated[str,
|
target: Annotated[str,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue