Skip to main content
Glama
manage_asset.py5.51 kB
""" Defines the manage_asset tool for interacting with Unity assets. """ import ast import asyncio import json from typing import Annotated, Any, Literal from fastmcp import Context from services.registry import mcp_for_unity_tool from services.tools import get_unity_instance_from_context from transport.unity_transport import send_with_unity_instance from transport.legacy.unity_connection import async_send_command_with_retry @mcp_for_unity_tool( description="Performs asset operations (import, create, modify, delete, etc.) in Unity." ) async def manage_asset( ctx: Context, action: Annotated[Literal["import", "create", "modify", "delete", "duplicate", "move", "rename", "search", "get_info", "create_folder", "get_components"], "Perform CRUD operations on assets."], 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] | 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, "Generate a preview/thumbnail for the asset when supported."] = False, search_pattern: Annotated[str, "Search pattern (e.g., '*.prefab')."] | None = None, filter_type: Annotated[str, "Filter type for search"] | None = None, filter_date_after: Annotated[str, "Date after which to filter"] | None = None, page_size: Annotated[int | float | str, "Page size for pagination"] | None = None, page_number: Annotated[int | float | str, "Page number for pagination"] | None = None, ) -> dict[str, Any]: unity_instance = get_unity_instance_from_context(ctx) def _parse_properties_string(raw: str) -> tuple[dict[str, Any] | None, str | None]: try: parsed = json.loads(raw) if not isinstance(parsed, dict): return None, f"manage_asset: properties JSON must decode to a dictionary; received {type(parsed)}" return parsed, "JSON" except json.JSONDecodeError as json_err: try: parsed = ast.literal_eval(raw) if not isinstance(parsed, dict): return None, f"manage_asset: properties string must evaluate to a dictionary; received {type(parsed)}" return parsed, "Python literal" except (ValueError, SyntaxError) as literal_err: return None, f"manage_asset: failed to parse properties string. JSON error: {json_err}; literal_eval error: {literal_err}" async def _normalize_properties(raw: dict[str, Any] | str | None) -> tuple[dict[str, Any] | None, str | None]: if raw is None: return {}, None if isinstance(raw, dict): await ctx.info(f"manage_asset: received properties as dict with keys: {list(raw.keys())}") return raw, None if isinstance(raw, str): await ctx.info(f"manage_asset: received properties as string (first 100 chars): {raw[:100]}") parsed, source = _parse_properties_string(raw) if parsed is None: return None, source await ctx.info(f"manage_asset: coerced properties from {source} string to dict") return parsed, None return None, f"manage_asset: properties must be a dict or JSON string; received {type(raw)}" properties, parse_error = await _normalize_properties(properties) if parse_error: await ctx.warn(parse_error) return {"success": False, "message": parse_error} # Coerce numeric inputs defensively def _coerce_int(value, default=None): if value is None: return default try: if isinstance(value, bool): return default if isinstance(value, int): return int(value) s = str(value).strip() if s.lower() in ("", "none", "null"): return default return int(float(s)) except Exception: return default page_size = _coerce_int(page_size) page_number = _coerce_int(page_number) # Prepare parameters for the C# handler params_dict = { "action": action.lower(), "path": path, "assetType": asset_type, "properties": properties, "destination": destination, "generatePreview": generate_preview, "searchPattern": search_pattern, "filterType": filter_type, "filterDateAfter": filter_date_after, "pageSize": page_size, "pageNumber": page_number } # Remove None values to avoid sending unnecessary nulls params_dict = {k: v for k, v in params_dict.items() if v is not None} # Get the current asyncio event loop loop = asyncio.get_running_loop() # Use centralized async retry helper with instance routing result = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_asset", params_dict, loop=loop) # Return the result obtained from Unity return result if isinstance(result, dict) else {"success": False, "message": str(result)}

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/CoplayDev/unity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server