From c07c762d452ca3d1a28e75aa230ad52197f93904 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Fri, 21 Nov 2025 12:06:38 -0800 Subject: [PATCH] fix(manage_asset): improve properties parameter parsing with fallback handling - Update type annotation to accept dict[str, Any] | str for properties parameter - Add robust parsing logic that tries JSON first, then ast.literal_eval for Python dict strings - Log warning and pass through unparseable strings to Unity (for backward compatibility) - Add detailed logging to track how properties are received and parsed - Fix syntax errors in test_telemetry.py (missing 'from' in import statements) - All 82 tests now pass successfully --- Server/src/services/tools/manage_asset.py | 27 ++++++++++++++++++----- Server/test_telemetry.py | 8 +++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Server/src/services/tools/manage_asset.py b/Server/src/services/tools/manage_asset.py index d6d40e4c..ad9209e8 100644 --- a/Server/src/services/tools/manage_asset.py +++ b/Server/src/services/tools/manage_asset.py @@ -21,8 +21,8 @@ async def manage_asset( path: Annotated[str, "Asset path (e.g., 'Materials/MyMaterial.mat') or search scope."], asset_type: Annotated[str, "Asset type (e.g., 'Material', 'Folder') - required for 'create'."] | None = None, - properties: Annotated[dict[str, Any], - "Dictionary of properties for 'create'/'modify'."] | None = None, + properties: Annotated[dict[str, Any] | str, + "Dictionary (or JSON string) of properties for 'create'/'modify'."] | None = None, destination: Annotated[str, "Target path for 'duplicate'/'move'."] | None = None, generate_preview: Annotated[bool, @@ -41,14 +41,29 @@ async def manage_asset( # Removed session_state import unity_instance = get_unity_instance_from_context(ctx) # Coerce 'properties' from JSON string to dict for client compatibility + # FastMCP may pass dicts directly (which we use as-is) or strings (which we parse) if isinstance(properties, str): + # Debug: log what we received + ctx.info(f"manage_asset: received properties as string (first 100 chars): {properties[:100]}") + # Try parsing as JSON first try: properties = json.loads(properties) ctx.info("manage_asset: coerced properties from JSON string to dict") - except Exception as e: - ctx.warn( - f"manage_asset: failed to parse properties JSON string: {e}") - # Leave properties as-is; Unity side may handle defaults + except json.JSONDecodeError as json_err: + # If JSON parsing fails, it might be a Python dict string representation + # Try using ast.literal_eval as a fallback (safer than eval) + try: + import ast + properties = ast.literal_eval(properties) + ctx.info("manage_asset: coerced properties from Python dict string to dict") + except (ValueError, SyntaxError) as eval_err: + # If both fail, log warning and leave as string - Unity side may handle or provide defaults + ctx.info(f"manage_asset: failed to parse properties string. JSON error: {json_err}, eval error: {eval_err}. Leaving as-is for Unity to handle.") + elif isinstance(properties, dict): + # Already a dict, use as-is + ctx.info(f"manage_asset: received properties as dict with keys: {list(properties.keys())}") + elif properties is not None: + ctx.info(f"manage_asset: received properties as unexpected type: {type(properties)}") # Ensure properties is a dict if None if properties is None: properties = {} diff --git a/Server/test_telemetry.py b/Server/test_telemetry.py index d6938d33..afc68662 100644 --- a/Server/test_telemetry.py +++ b/Server/test_telemetry.py @@ -70,10 +70,10 @@ def test_telemetry_disabled(): # Re-import to get fresh config import importlib - import telemetry - importlib.reload(telemetry) + import core.telemetry + importlib.reload(core.telemetry) - core.telemetry import is_telemetry_enabled, record_telemetry, RecordType + from core.telemetry import is_telemetry_enabled, record_telemetry, RecordType _ = is_telemetry_enabled() @@ -95,7 +95,7 @@ def test_data_storage(): # Silent for tests try: - core.telemetry import get_telemetry + from core.telemetry import get_telemetry collector = get_telemetry() data_dir = collector.config.data_dir