mcpforunityserver 9.4.0b20260203025228__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. cli/__init__.py +3 -0
  2. cli/commands/__init__.py +3 -0
  3. cli/commands/animation.py +84 -0
  4. cli/commands/asset.py +280 -0
  5. cli/commands/audio.py +125 -0
  6. cli/commands/batch.py +171 -0
  7. cli/commands/code.py +182 -0
  8. cli/commands/component.py +190 -0
  9. cli/commands/editor.py +447 -0
  10. cli/commands/gameobject.py +487 -0
  11. cli/commands/instance.py +93 -0
  12. cli/commands/lighting.py +123 -0
  13. cli/commands/material.py +239 -0
  14. cli/commands/prefab.py +248 -0
  15. cli/commands/scene.py +231 -0
  16. cli/commands/script.py +222 -0
  17. cli/commands/shader.py +226 -0
  18. cli/commands/texture.py +540 -0
  19. cli/commands/tool.py +58 -0
  20. cli/commands/ui.py +258 -0
  21. cli/commands/vfx.py +421 -0
  22. cli/main.py +281 -0
  23. cli/utils/__init__.py +31 -0
  24. cli/utils/config.py +58 -0
  25. cli/utils/confirmation.py +37 -0
  26. cli/utils/connection.py +254 -0
  27. cli/utils/constants.py +23 -0
  28. cli/utils/output.py +195 -0
  29. cli/utils/parsers.py +112 -0
  30. cli/utils/suggestions.py +34 -0
  31. core/__init__.py +0 -0
  32. core/config.py +67 -0
  33. core/constants.py +4 -0
  34. core/logging_decorator.py +37 -0
  35. core/telemetry.py +551 -0
  36. core/telemetry_decorator.py +164 -0
  37. main.py +845 -0
  38. mcpforunityserver-9.4.0b20260203025228.dist-info/METADATA +328 -0
  39. mcpforunityserver-9.4.0b20260203025228.dist-info/RECORD +105 -0
  40. mcpforunityserver-9.4.0b20260203025228.dist-info/WHEEL +5 -0
  41. mcpforunityserver-9.4.0b20260203025228.dist-info/entry_points.txt +3 -0
  42. mcpforunityserver-9.4.0b20260203025228.dist-info/licenses/LICENSE +21 -0
  43. mcpforunityserver-9.4.0b20260203025228.dist-info/top_level.txt +7 -0
  44. models/__init__.py +4 -0
  45. models/models.py +56 -0
  46. models/unity_response.py +70 -0
  47. services/__init__.py +0 -0
  48. services/api_key_service.py +235 -0
  49. services/custom_tool_service.py +499 -0
  50. services/registry/__init__.py +22 -0
  51. services/registry/resource_registry.py +53 -0
  52. services/registry/tool_registry.py +51 -0
  53. services/resources/__init__.py +86 -0
  54. services/resources/active_tool.py +48 -0
  55. services/resources/custom_tools.py +57 -0
  56. services/resources/editor_state.py +304 -0
  57. services/resources/gameobject.py +243 -0
  58. services/resources/layers.py +30 -0
  59. services/resources/menu_items.py +35 -0
  60. services/resources/prefab.py +191 -0
  61. services/resources/prefab_stage.py +40 -0
  62. services/resources/project_info.py +40 -0
  63. services/resources/selection.py +56 -0
  64. services/resources/tags.py +31 -0
  65. services/resources/tests.py +88 -0
  66. services/resources/unity_instances.py +125 -0
  67. services/resources/windows.py +48 -0
  68. services/state/external_changes_scanner.py +245 -0
  69. services/tools/__init__.py +83 -0
  70. services/tools/batch_execute.py +93 -0
  71. services/tools/debug_request_context.py +86 -0
  72. services/tools/execute_custom_tool.py +43 -0
  73. services/tools/execute_menu_item.py +32 -0
  74. services/tools/find_gameobjects.py +110 -0
  75. services/tools/find_in_file.py +181 -0
  76. services/tools/manage_asset.py +119 -0
  77. services/tools/manage_components.py +131 -0
  78. services/tools/manage_editor.py +64 -0
  79. services/tools/manage_gameobject.py +260 -0
  80. services/tools/manage_material.py +111 -0
  81. services/tools/manage_prefabs.py +209 -0
  82. services/tools/manage_scene.py +111 -0
  83. services/tools/manage_script.py +645 -0
  84. services/tools/manage_scriptable_object.py +87 -0
  85. services/tools/manage_shader.py +71 -0
  86. services/tools/manage_texture.py +581 -0
  87. services/tools/manage_vfx.py +120 -0
  88. services/tools/preflight.py +110 -0
  89. services/tools/read_console.py +151 -0
  90. services/tools/refresh_unity.py +153 -0
  91. services/tools/run_tests.py +317 -0
  92. services/tools/script_apply_edits.py +1006 -0
  93. services/tools/set_active_instance.py +120 -0
  94. services/tools/utils.py +348 -0
  95. transport/__init__.py +0 -0
  96. transport/legacy/port_discovery.py +329 -0
  97. transport/legacy/stdio_port_registry.py +65 -0
  98. transport/legacy/unity_connection.py +910 -0
  99. transport/models.py +68 -0
  100. transport/plugin_hub.py +787 -0
  101. transport/plugin_registry.py +182 -0
  102. transport/unity_instance_middleware.py +262 -0
  103. transport/unity_transport.py +94 -0
  104. utils/focus_nudge.py +589 -0
  105. utils/module_discovery.py +55 -0
@@ -0,0 +1,540 @@
1
+ """Texture CLI commands."""
2
+
3
+ import sys
4
+ import click
5
+ from typing import Optional, Any
6
+
7
+ from cli.utils.config import get_config
8
+ from cli.utils.output import format_output, print_error, print_success
9
+ from cli.utils.connection import run_command, handle_unity_errors
10
+ from cli.utils.parsers import parse_json_or_exit as try_parse_json
11
+
12
+
13
+ _TEXTURE_TYPES = {
14
+ "default": "Default",
15
+ "normal_map": "NormalMap",
16
+ "editor_gui": "GUI",
17
+ "sprite": "Sprite",
18
+ "cursor": "Cursor",
19
+ "cookie": "Cookie",
20
+ "lightmap": "Lightmap",
21
+ "directional_lightmap": "DirectionalLightmap",
22
+ "shadow_mask": "Shadowmask",
23
+ "single_channel": "SingleChannel",
24
+ }
25
+
26
+ _TEXTURE_SHAPES = {"2d": "Texture2D", "cube": "TextureCube"}
27
+
28
+ _ALPHA_SOURCES = {
29
+ "none": "None",
30
+ "from_input": "FromInput",
31
+ "from_gray_scale": "FromGrayScale",
32
+ }
33
+
34
+ _WRAP_MODES = {
35
+ "repeat": "Repeat",
36
+ "clamp": "Clamp",
37
+ "mirror": "Mirror",
38
+ "mirror_once": "MirrorOnce",
39
+ }
40
+
41
+ _FILTER_MODES = {"point": "Point",
42
+ "bilinear": "Bilinear", "trilinear": "Trilinear"}
43
+
44
+ _COMPRESSIONS = {
45
+ "none": "Uncompressed",
46
+ "low_quality": "CompressedLQ",
47
+ "normal_quality": "Compressed",
48
+ "high_quality": "CompressedHQ",
49
+ }
50
+
51
+ _SPRITE_MODES = {"single": "Single",
52
+ "multiple": "Multiple", "polygon": "Polygon"}
53
+
54
+ _SPRITE_MESH_TYPES = {"full_rect": "FullRect", "tight": "Tight"}
55
+
56
+ _MIPMAP_FILTERS = {"box": "BoxFilter", "kaiser": "KaiserFilter"}
57
+
58
+ _MAX_TEXTURE_DIMENSION = 1024
59
+ _MAX_TEXTURE_PIXELS = 1024 * 1024
60
+
61
+
62
+ def _validate_texture_dimensions(width: int, height: int) -> list[str]:
63
+ if width <= 0 or height <= 0:
64
+ raise ValueError("width and height must be positive")
65
+ warnings: list[str] = []
66
+ if width > _MAX_TEXTURE_DIMENSION or height > _MAX_TEXTURE_DIMENSION:
67
+ warnings.append(
68
+ f"width and height should be <= {_MAX_TEXTURE_DIMENSION} (got {width}x{height})")
69
+ total_pixels = width * height
70
+ if total_pixels > _MAX_TEXTURE_PIXELS:
71
+ warnings.append(
72
+ f"width*height should be <= {_MAX_TEXTURE_PIXELS} (got {width}x{height})")
73
+ return warnings
74
+
75
+
76
+ def _is_normalized_color(values: list[Any]) -> bool:
77
+ if not values:
78
+ return False
79
+
80
+ try:
81
+ numeric_values = [float(v) for v in values]
82
+ except (TypeError, ValueError):
83
+ return False
84
+
85
+ all_small = all(0 <= v <= 1.0 for v in numeric_values)
86
+ if not all_small:
87
+ return False
88
+
89
+ has_fractional = any(0 < v < 1 for v in numeric_values)
90
+ all_binary = all(v in (0, 1, 0.0, 1.0) for v in numeric_values)
91
+
92
+ return has_fractional or all_binary
93
+
94
+
95
+ def _parse_hex_color(value: str) -> list[int]:
96
+ h = value.lstrip("#")
97
+ if len(h) == 6:
98
+ return [int(h[i:i + 2], 16) for i in (0, 2, 4)] + [255]
99
+ if len(h) == 8:
100
+ return [int(h[i:i + 2], 16) for i in (0, 2, 4, 6)]
101
+ raise ValueError(f"Invalid hex color: {value}")
102
+
103
+
104
+ def _normalize_color(value: Any, context: str) -> list[int]:
105
+ if value is None:
106
+ raise ValueError(f"{context} is required")
107
+
108
+ if isinstance(value, str):
109
+ if value.startswith("#"):
110
+ return _parse_hex_color(value)
111
+ value = try_parse_json(value, context)
112
+
113
+ # Handle dict with r/g/b keys (e.g., {"r": 1, "g": 0, "b": 0} or {"r": 1, "g": 0, "b": 0, "a": 1})
114
+ if isinstance(value, dict):
115
+ if all(k in value for k in ("r", "g", "b")):
116
+ try:
117
+ color = [value["r"], value["g"], value["b"]]
118
+ if "a" in value:
119
+ color.append(value["a"])
120
+ else:
121
+ color.append(1.0 if _is_normalized_color(color) else 255)
122
+ if _is_normalized_color(color):
123
+ return [int(round(float(c) * 255)) for c in color]
124
+ return [int(c) for c in color]
125
+ except (TypeError, ValueError):
126
+ raise ValueError(f"{context} dict values must be numeric, got {value}")
127
+ raise ValueError(f"{context} dict must have 'r', 'g', 'b' keys, got {list(value.keys())}")
128
+
129
+ if isinstance(value, (list, tuple)):
130
+ if len(value) == 3:
131
+ value = list(value) + [1.0 if _is_normalized_color(value) else 255]
132
+ if len(value) == 4:
133
+ try:
134
+ if _is_normalized_color(value):
135
+ return [int(round(float(c) * 255)) for c in value]
136
+ return [int(c) for c in value]
137
+ except (TypeError, ValueError):
138
+ raise ValueError(
139
+ f"{context} values must be numeric, got {value}")
140
+ raise ValueError(
141
+ f"{context} must have 3 or 4 components, got {len(value)}")
142
+
143
+ raise ValueError(f"{context} must be a list or hex string")
144
+
145
+
146
+ def _normalize_palette(value: Any, context: str) -> list[list[int]]:
147
+ if value is None:
148
+ return []
149
+ if isinstance(value, str):
150
+ value = try_parse_json(value, context)
151
+ if not isinstance(value, list):
152
+ raise ValueError(f"{context} must be a list of colors")
153
+ return [_normalize_color(color, f"{context} item") for color in value]
154
+
155
+
156
+ def _normalize_pixels(value: Any, width: int, height: int, context: str) -> list[list[int]] | str:
157
+ if value is None:
158
+ raise ValueError(f"{context} is required")
159
+ if isinstance(value, str):
160
+ if value.startswith("base64:"):
161
+ return value
162
+ trimmed = value.strip()
163
+ if trimmed.startswith("[") and trimmed.endswith("]"):
164
+ value = try_parse_json(trimmed, context)
165
+ else:
166
+ return f"base64:{value}"
167
+ if isinstance(value, list):
168
+ expected_count = width * height
169
+ if len(value) != expected_count:
170
+ raise ValueError(
171
+ f"{context} must have {expected_count} entries, got {len(value)}")
172
+ return [_normalize_color(pixel, f"{context} pixel") for pixel in value]
173
+ raise ValueError(f"{context} must be a list or base64 string")
174
+
175
+
176
+ def _normalize_set_pixels(value: Any) -> dict[str, Any]:
177
+ if value is None:
178
+ raise ValueError("set-pixels is required")
179
+ if isinstance(value, str):
180
+ value = try_parse_json(value, "set-pixels")
181
+ if not isinstance(value, dict):
182
+ raise ValueError("set-pixels must be a JSON object")
183
+
184
+ result: dict[str, Any] = dict(value)
185
+
186
+ if "pixels" in value:
187
+ width = value.get("width")
188
+ height = value.get("height")
189
+ if width is None or height is None:
190
+ raise ValueError(
191
+ "set-pixels requires width and height when pixels are provided")
192
+ width = int(width)
193
+ height = int(height)
194
+ if width <= 0 or height <= 0:
195
+ raise ValueError("set-pixels width and height must be positive")
196
+ result["width"] = width
197
+ result["height"] = height
198
+ result["pixels"] = _normalize_pixels(
199
+ value["pixels"], width, height, "set-pixels pixels")
200
+
201
+ if "color" in value:
202
+ result["color"] = _normalize_color(value["color"], "set-pixels color")
203
+
204
+ if "pixels" not in value and "color" not in value:
205
+ raise ValueError("set-pixels requires 'color' or 'pixels'")
206
+
207
+ if "x" in value:
208
+ result["x"] = int(value["x"])
209
+ if "y" in value:
210
+ result["y"] = int(value["y"])
211
+
212
+ if "width" in value and "pixels" not in value:
213
+ result["width"] = int(value["width"])
214
+ if "height" in value and "pixels" not in value:
215
+ result["height"] = int(value["height"])
216
+
217
+ return result
218
+
219
+
220
+ def _map_enum(value: Any, mapping: dict[str, str]) -> Any:
221
+ if isinstance(value, str):
222
+ key = value.lower()
223
+ return mapping.get(key, value)
224
+ return value
225
+
226
+
227
+ _TRUE_STRINGS = {"true", "1", "yes", "on"}
228
+ _FALSE_STRINGS = {"false", "0", "no", "off"}
229
+
230
+
231
+ def _coerce_bool(value: Any, name: str) -> bool:
232
+ if isinstance(value, bool):
233
+ return value
234
+ if isinstance(value, (int, float)) and value in (0, 1, 0.0, 1.0):
235
+ return bool(value)
236
+ if isinstance(value, str):
237
+ lowered = value.strip().lower()
238
+ if lowered in _TRUE_STRINGS:
239
+ return True
240
+ if lowered in _FALSE_STRINGS:
241
+ return False
242
+ raise ValueError(f"{name} must be a boolean")
243
+
244
+
245
+ def _normalize_import_settings(value: Any) -> dict[str, Any]:
246
+ if value is None:
247
+ return {}
248
+ if isinstance(value, str):
249
+ value = try_parse_json(value, "import_settings")
250
+ if not isinstance(value, dict):
251
+ raise ValueError("import_settings must be a JSON object")
252
+
253
+ result: dict[str, Any] = {}
254
+
255
+ if "texture_type" in value:
256
+ result["textureType"] = _map_enum(
257
+ value["texture_type"], _TEXTURE_TYPES)
258
+ if "texture_shape" in value:
259
+ result["textureShape"] = _map_enum(
260
+ value["texture_shape"], _TEXTURE_SHAPES)
261
+
262
+ for snake, camel in [
263
+ ("srgb", "sRGBTexture"),
264
+ ("alpha_is_transparency", "alphaIsTransparency"),
265
+ ("readable", "isReadable"),
266
+ ("generate_mipmaps", "mipmapEnabled"),
267
+ ("compression_crunched", "crunchedCompression"),
268
+ ]:
269
+ if snake in value:
270
+ result[camel] = _coerce_bool(value[snake], snake)
271
+
272
+ if "alpha_source" in value:
273
+ result["alphaSource"] = _map_enum(
274
+ value["alpha_source"], _ALPHA_SOURCES)
275
+
276
+ for snake, camel in [("wrap_mode", "wrapMode"), ("wrap_mode_u", "wrapModeU"), ("wrap_mode_v", "wrapModeV")]:
277
+ if snake in value:
278
+ result[camel] = _map_enum(value[snake], _WRAP_MODES)
279
+
280
+ if "filter_mode" in value:
281
+ result["filterMode"] = _map_enum(value["filter_mode"], _FILTER_MODES)
282
+ if "mipmap_filter" in value:
283
+ result["mipmapFilter"] = _map_enum(
284
+ value["mipmap_filter"], _MIPMAP_FILTERS)
285
+ if "compression" in value:
286
+ result["textureCompression"] = _map_enum(
287
+ value["compression"], _COMPRESSIONS)
288
+
289
+ if "aniso_level" in value:
290
+ result["anisoLevel"] = int(value["aniso_level"])
291
+ if "max_texture_size" in value:
292
+ result["maxTextureSize"] = int(value["max_texture_size"])
293
+ if "compression_quality" in value:
294
+ result["compressionQuality"] = int(value["compression_quality"])
295
+
296
+ if "sprite_mode" in value:
297
+ result["spriteImportMode"] = _map_enum(
298
+ value["sprite_mode"], _SPRITE_MODES)
299
+ if "sprite_pixels_per_unit" in value:
300
+ result["spritePixelsPerUnit"] = float(value["sprite_pixels_per_unit"])
301
+ if "sprite_pivot" in value:
302
+ result["spritePivot"] = value["sprite_pivot"]
303
+ if "sprite_mesh_type" in value:
304
+ result["spriteMeshType"] = _map_enum(
305
+ value["sprite_mesh_type"], _SPRITE_MESH_TYPES)
306
+ if "sprite_extrude" in value:
307
+ result["spriteExtrude"] = int(value["sprite_extrude"])
308
+
309
+ for key, val in value.items():
310
+ if key in result:
311
+ continue
312
+ if key in (
313
+ "textureType", "textureShape", "sRGBTexture", "alphaSource",
314
+ "alphaIsTransparency", "isReadable", "mipmapEnabled", "wrapMode",
315
+ "wrapModeU", "wrapModeV", "filterMode", "mipmapFilter", "anisoLevel",
316
+ "maxTextureSize", "textureCompression", "crunchedCompression",
317
+ "compressionQuality", "spriteImportMode", "spritePixelsPerUnit",
318
+ "spritePivot", "spriteMeshType", "spriteExtrude",
319
+ ):
320
+ result[key] = val
321
+
322
+ return result
323
+
324
+
325
+ @click.group()
326
+ def texture():
327
+ """Texture operations - create, modify, generate sprites."""
328
+ pass
329
+
330
+
331
+ @texture.command("create")
332
+ @click.argument("path")
333
+ @click.option("--width", default=64, help="Texture width (default: 64)")
334
+ @click.option("--height", default=64, help="Texture height (default: 64)")
335
+ @click.option("--image-path", help="Source image path (PNG/JPG) to import.")
336
+ @click.option("--color", help="Fill color (e.g., '#FF0000' or '[1,0,0,1]')")
337
+ @click.option("--pattern", type=click.Choice([
338
+ "checkerboard", "stripes", "stripes_h", "stripes_v", "stripes_diag",
339
+ "dots", "grid", "brick"
340
+ ]), help="Pattern type")
341
+ @click.option("--palette", help="Color palette for pattern (JSON array of colors)")
342
+ @click.option("--import-settings", help="TextureImporter settings (JSON)")
343
+ @handle_unity_errors
344
+ def create(path: str, width: int, height: int, image_path: Optional[str], color: Optional[str],
345
+ pattern: Optional[str], palette: Optional[str], import_settings: Optional[str]):
346
+ """Create a new procedural texture.
347
+
348
+ \b
349
+ Examples:
350
+ unity-mcp texture create Assets/Red.png --color '[255,0,0]'
351
+ unity-mcp texture create Assets/Check.png --pattern checkerboard
352
+ unity-mcp texture create Assets/UI.png --import-settings '{"texture_type": "sprite"}'
353
+ """
354
+ config = get_config()
355
+ if image_path:
356
+ if color or pattern or palette:
357
+ print_error(
358
+ "image-path cannot be combined with color, pattern, or palette.")
359
+ sys.exit(1)
360
+ else:
361
+ try:
362
+ warnings = _validate_texture_dimensions(width, height)
363
+ except ValueError as e:
364
+ print_error(str(e))
365
+ sys.exit(1)
366
+ for warning in warnings:
367
+ click.echo(f"⚠️ Warning: {warning}")
368
+
369
+ params: dict[str, Any] = {
370
+ "action": "create",
371
+ "path": path,
372
+ "width": width,
373
+ "height": height,
374
+ }
375
+
376
+ if color:
377
+ try:
378
+ params["fillColor"] = _normalize_color(color, "color")
379
+ except ValueError as e:
380
+ print_error(str(e))
381
+ sys.exit(1)
382
+ elif not pattern and not image_path:
383
+ # Default to white if no color or pattern specified
384
+ params["fillColor"] = [255, 255, 255, 255]
385
+
386
+ if pattern:
387
+ params["pattern"] = pattern
388
+
389
+ if palette:
390
+ try:
391
+ params["palette"] = _normalize_palette(palette, "palette")
392
+ except ValueError as e:
393
+ print_error(str(e))
394
+ sys.exit(1)
395
+
396
+ if import_settings:
397
+ try:
398
+ params["importSettings"] = _normalize_import_settings(
399
+ import_settings)
400
+ except ValueError as e:
401
+ print_error(str(e))
402
+ sys.exit(1)
403
+
404
+ if image_path:
405
+ params["imagePath"] = image_path
406
+
407
+ result = run_command("manage_texture", params, config)
408
+ click.echo(format_output(result, config.format))
409
+ if result.get("success"):
410
+ print_success(f"Created texture: {path}")
411
+
412
+
413
+ @texture.command("sprite")
414
+ @click.argument("path")
415
+ @click.option("--width", default=64, help="Texture width (default: 64)")
416
+ @click.option("--height", default=64, help="Texture height (default: 64)")
417
+ @click.option("--image-path", help="Source image path (PNG/JPG) to import.")
418
+ @click.option("--color", help="Fill color (e.g., '#FF0000' or '[1,0,0,1]')")
419
+ @click.option("--pattern", type=click.Choice([
420
+ "checkerboard", "stripes", "dots", "grid"
421
+ ]), help="Pattern type (defaults to checkerboard if no color specified)")
422
+ @click.option("--ppu", default=100.0, help="Pixels Per Unit")
423
+ @click.option("--pivot", help="Pivot as [x,y] (default: [0.5, 0.5])")
424
+ @handle_unity_errors
425
+ def sprite(path: str, width: int, height: int, image_path: Optional[str], color: Optional[str], pattern: Optional[str], ppu: float, pivot: Optional[str]):
426
+ """Quickly create a sprite texture.
427
+
428
+ \b
429
+ Examples:
430
+ unity-mcp texture sprite Assets/Sprites/Player.png
431
+ unity-mcp texture sprite Assets/Sprites/Coin.png --pattern dots
432
+ unity-mcp texture sprite Assets/Sprites/Solid.png --color '[0,255,0]'
433
+ """
434
+ config = get_config()
435
+ if image_path:
436
+ if color or pattern:
437
+ print_error("image-path cannot be combined with color or pattern.")
438
+ sys.exit(1)
439
+ else:
440
+ try:
441
+ warnings = _validate_texture_dimensions(width, height)
442
+ except ValueError as e:
443
+ print_error(str(e))
444
+ sys.exit(1)
445
+ for warning in warnings:
446
+ click.echo(f"⚠️ Warning: {warning}")
447
+
448
+ sprite_settings: dict[str, Any] = {"pixelsPerUnit": ppu}
449
+ if pivot:
450
+ sprite_settings["pivot"] = try_parse_json(pivot, "pivot")
451
+ else:
452
+ sprite_settings["pivot"] = [0.5, 0.5]
453
+
454
+ params: dict[str, Any] = {
455
+ "action": "create_sprite",
456
+ "path": path,
457
+ "width": width,
458
+ "height": height,
459
+ "spriteSettings": sprite_settings
460
+ }
461
+
462
+ if color:
463
+ try:
464
+ params["fillColor"] = _normalize_color(color, "color")
465
+ except ValueError as e:
466
+ print_error(str(e))
467
+ sys.exit(1)
468
+
469
+ # Only default pattern if no color is specified
470
+ if pattern:
471
+ params["pattern"] = pattern
472
+ elif not color and not image_path:
473
+ params["pattern"] = "checkerboard"
474
+
475
+ if image_path:
476
+ params["imagePath"] = image_path
477
+
478
+ result = run_command("manage_texture", params, config)
479
+ click.echo(format_output(result, config.format))
480
+ if result.get("success"):
481
+ print_success(f"Created sprite: {path}")
482
+
483
+
484
+ @texture.command("modify")
485
+ @click.argument("path")
486
+ @click.option("--set-pixels", required=True, help="Modification args as JSON")
487
+ @handle_unity_errors
488
+ def modify(path: str, set_pixels: str):
489
+ """Modify an existing texture.
490
+
491
+ \b
492
+ Examples:
493
+ unity-mcp texture modify Assets/Tex.png --set-pixels '{"x":0,"y":0,"width":10,"height":10,"color":[255,0,0]}'
494
+ unity-mcp texture modify Assets/Tex.png --set-pixels '{"x":0,"y":0,"width":2,"height":2,"pixels":[[255,0,0,255],[0,255,0,255],[0,0,255,255],[255,255,0,255]]}'
495
+ """
496
+ config = get_config()
497
+
498
+ params: dict[str, Any] = {
499
+ "action": "modify",
500
+ "path": path,
501
+ }
502
+
503
+ try:
504
+ params["setPixels"] = _normalize_set_pixels(set_pixels)
505
+ except ValueError as e:
506
+ print_error(str(e))
507
+ sys.exit(1)
508
+
509
+ result = run_command("manage_texture", params, config)
510
+ click.echo(format_output(result, config.format))
511
+ if result.get("success"):
512
+ print_success(f"Modified texture: {path}")
513
+
514
+
515
+ @texture.command("delete")
516
+ @click.argument("path")
517
+ @click.option(
518
+ "--force", "-f",
519
+ is_flag=True,
520
+ help="Skip confirmation prompt."
521
+ )
522
+ @handle_unity_errors
523
+ def delete(path: str, force: bool):
524
+ """Delete a texture.
525
+
526
+ \\b
527
+ Examples:
528
+ unity-mcp texture delete "Assets/Textures/Old.png"
529
+ unity-mcp texture delete "Assets/Textures/Old.png" --force
530
+ """
531
+ from cli.utils.confirmation import confirm_destructive_action
532
+ config = get_config()
533
+
534
+ confirm_destructive_action("Delete", "texture", path, force)
535
+
536
+ result = run_command("manage_texture", {
537
+ "action": "delete", "path": path}, config)
538
+ click.echo(format_output(result, config.format))
539
+ if result.get("success"):
540
+ print_success(f"Deleted texture: {path}")
cli/commands/tool.py ADDED
@@ -0,0 +1,58 @@
1
+ """Tool CLI commands for listing custom tools."""
2
+
3
+ import click
4
+
5
+ from cli.utils.config import get_config
6
+ from cli.utils.output import format_output, print_error
7
+ from cli.utils.connection import run_list_custom_tools, handle_unity_errors
8
+
9
+
10
+ def _list_custom_tools() -> None:
11
+ config = get_config()
12
+ result = run_list_custom_tools(config)
13
+ if config.format != "text":
14
+ click.echo(format_output(result, config.format))
15
+ return
16
+
17
+ if not isinstance(result, dict) or not result.get("success", True):
18
+ click.echo(format_output(result, config.format))
19
+ return
20
+
21
+ tools = result.get("tools")
22
+ if tools is None:
23
+ data = result.get("data", {})
24
+ tools = data.get("tools") if isinstance(data, dict) else None
25
+ if not isinstance(tools, list):
26
+ click.echo(format_output(result, config.format))
27
+ return
28
+
29
+ click.echo(f"Custom tools ({len(tools)}):")
30
+ for i, t in enumerate(tools):
31
+ name = t.get("name") if isinstance(t, dict) else str(t)
32
+ click.echo(f" [{i}] {name}")
33
+
34
+
35
+ @click.group("tool")
36
+ def tool():
37
+ """Tool management - list custom tools for the active Unity project."""
38
+ pass
39
+
40
+
41
+ @tool.command("list")
42
+ @handle_unity_errors
43
+ def list_tools():
44
+ """List custom tools registered for the active Unity project."""
45
+ _list_custom_tools()
46
+
47
+
48
+ @click.group("custom_tool")
49
+ def custom_tool():
50
+ """Alias for tool management (custom tools)."""
51
+ pass
52
+
53
+
54
+ @custom_tool.command("list")
55
+ @handle_unity_errors
56
+ def list_custom_tools():
57
+ """List custom tools registered for the active Unity project."""
58
+ _list_custom_tools()