From 57fb86fbca64e0478bff7c5c95466a126e7a6911 Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:02:45 -0500 Subject: [PATCH 1/2] Update for UI-based batch execution --- .claude/skills/unity-mcp-skill/SKILL.md | 1 + .../unity-mcp-skill/references/workflows.md | 407 ++++++++++++++++++ unity-mcp-skill/SKILL.md | 1 + unity-mcp-skill/references/workflows.md | 407 ++++++++++++++++++ 4 files changed, 816 insertions(+) diff --git a/.claude/skills/unity-mcp-skill/SKILL.md b/.claude/skills/unity-mcp-skill/SKILL.md index 40a8541d8..b9a76357e 100644 --- a/.claude/skills/unity-mcp-skill/SKILL.md +++ b/.claude/skills/unity-mcp-skill/SKILL.md @@ -124,6 +124,7 @@ uri="file:///full/path/to/file.cs" | **Editor** | `manage_editor`, `execute_menu_item`, `read_console` | Editor control | | **Testing** | `run_tests`, `get_test_job` | Unity Test Framework | | **Batch** | `batch_execute` | Parallel/bulk operations | +| **UI** | `batch_execute` with `manage_gameobject` + `manage_components` | Canvas, Panel, Button, Text, Slider, Toggle, Input Field (see [UI workflows](references/workflows.md#ui-creation-workflows)) | ## Common Workflows diff --git a/.claude/skills/unity-mcp-skill/references/workflows.md b/.claude/skills/unity-mcp-skill/references/workflows.md index f04245cf4..629becfa8 100644 --- a/.claude/skills/unity-mcp-skill/references/workflows.md +++ b/.claude/skills/unity-mcp-skill/references/workflows.md @@ -10,6 +10,7 @@ Common workflows and patterns for effective Unity-MCP usage. - [Asset Management Workflows](#asset-management-workflows) - [Testing Workflows](#testing-workflows) - [Debugging Workflows](#debugging-workflows) +- [UI Creation Workflows](#ui-creation-workflows) - [Batch Operations](#batch-operations) --- @@ -491,6 +492,412 @@ manage_scene(action="screenshot") --- +## UI Creation Workflows + +Unity UI (Canvas-based UGUI) requires specific component hierarchies. Use `batch_execute` with `fail_fast=True` to create complete UI elements in a single call. These templates handle the boilerplate so you don't need to remember which components each UI element needs. + +### Create Canvas (Foundation for All UI) + +Every UI element must be under a Canvas. A Canvas requires three components: `Canvas`, `CanvasScaler`, and `GraphicRaycaster`. + +```python +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "MainCanvas" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "MainCanvas", "component_type": "Canvas" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "MainCanvas", "component_type": "CanvasScaler" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "MainCanvas", "component_type": "GraphicRaycaster" + }}, + # renderMode: 0=ScreenSpaceOverlay, 1=ScreenSpaceCamera, 2=WorldSpace + {"tool": "manage_components", "params": { + "action": "set_property", "target": "MainCanvas", + "component_type": "Canvas", "property": "renderMode", "value": 0 + }}, + # CanvasScaler: uiScaleMode 1=ScaleWithScreenSize + {"tool": "manage_components", "params": { + "action": "set_property", "target": "MainCanvas", + "component_type": "CanvasScaler", "property": "uiScaleMode", "value": 1 + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "MainCanvas", + "component_type": "CanvasScaler", "property": "referenceResolution", + "value": [1920, 1080] + }} +]) +``` + +### Create EventSystem (Required Once Per Scene for UI Interaction) + +If no EventSystem exists in the scene, buttons and other interactive UI elements won't respond to input. Create one alongside your first Canvas. + +```python +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "EventSystem" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "EventSystem", + "component_type": "UnityEngine.EventSystems.EventSystem" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "EventSystem", + "component_type": "UnityEngine.InputSystem.UI.InputSystemUIInputModule" + }} +]) +``` + +> **Note:** For projects using legacy Input Manager instead of Input System, use `"component_type": "UnityEngine.EventSystems.StandaloneInputModule"` instead. + +### Create Panel (Background Container) + +A Panel is an Image component used as a background/container for other UI elements. + +```python +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "MenuPanel", "parent": "MainCanvas" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "MenuPanel", "component_type": "Image" + }}, + # Set semi-transparent dark background + {"tool": "manage_components", "params": { + "action": "set_property", "target": "MenuPanel", + "component_type": "Image", "property": "color", + "value": [0.1, 0.1, 0.1, 0.8] + }} +]) +``` + +### Create Text (TextMeshPro) + +TextMeshProUGUI automatically adds a RectTransform when added to a child of a Canvas. + +```python +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "TitleText", "parent": "MenuPanel" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "TitleText", + "component_type": "TextMeshProUGUI" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "TitleText", + "component_type": "TextMeshProUGUI", + "properties": { + "text": "My Game Title", + "fontSize": 48, + "alignment": 514, + "color": [1, 1, 1, 1] + } + }} +]) +``` + +> **TextMeshPro alignment values:** 257=TopLeft, 258=TopCenter, 260=TopRight, 513=MiddleLeft, 514=MiddleCenter, 516=MiddleRight, 1025=BottomLeft, 1026=BottomCenter, 1028=BottomRight. + +### Create Button (With Label) + +A Button needs an `Image` (visual) + `Button` (interaction) on the parent, and a child with `TextMeshProUGUI` for the label. + +```python +batch_execute(fail_fast=True, commands=[ + # Button container with Image + Button components + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "StartButton", "parent": "MenuPanel" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "StartButton", "component_type": "Image" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "StartButton", "component_type": "Button" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "StartButton", + "component_type": "Image", "property": "color", + "value": [0.2, 0.6, 1.0, 1.0] + }}, + # Child text label + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "StartButton_Label", "parent": "StartButton" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "StartButton_Label", + "component_type": "TextMeshProUGUI" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "StartButton_Label", + "component_type": "TextMeshProUGUI", + "properties": {"text": "Start Game", "fontSize": 24, "alignment": 514} + }} +]) +``` + +### Create Slider + +A Slider requires a specific hierarchy: the slider root, a background, a fill area with fill, and a handle area with handle. + +```python +batch_execute(fail_fast=True, commands=[ + # Slider root + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "HealthSlider", "parent": "MainCanvas" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "HealthSlider", "component_type": "Slider" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "HealthSlider", "component_type": "Image" + }}, + # Background + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Background", "parent": "HealthSlider" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Background", "component_type": "Image" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "Background", + "component_type": "Image", "property": "color", + "value": [0.3, 0.3, 0.3, 1.0] + }}, + # Fill Area + Fill + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Fill Area", "parent": "HealthSlider" + }}, + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Fill", "parent": "Fill Area" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Fill", "component_type": "Image" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "Fill", + "component_type": "Image", "property": "color", + "value": [0.2, 0.8, 0.2, 1.0] + }}, + # Handle Area + Handle + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Handle Slide Area", "parent": "HealthSlider" + }}, + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Handle", "parent": "Handle Slide Area" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Handle", "component_type": "Image" + }} +]) +``` + +### Create Input Field (TextMeshPro) + +```python +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "NameInput", "parent": "MenuPanel" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "NameInput", "component_type": "Image" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "NameInput", + "component_type": "TMP_InputField" + }}, + # Text area child + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Text Area", "parent": "NameInput" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Text Area", + "component_type": "RectMask2D" + }}, + # Placeholder + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Placeholder", "parent": "Text Area" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Placeholder", + "component_type": "TextMeshProUGUI" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "Placeholder", + "component_type": "TextMeshProUGUI", + "properties": {"text": "Enter name...", "fontStyle": 2, "color": [0.5, 0.5, 0.5, 0.5]} + }}, + # Actual text + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Text", "parent": "Text Area" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Text", + "component_type": "TextMeshProUGUI" + }} +]) +``` + +### Create Toggle (Checkbox) + +```python +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "SoundToggle", "parent": "MenuPanel" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "SoundToggle", "component_type": "Toggle" + }}, + # Background box + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Background", "parent": "SoundToggle" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Background", "component_type": "Image" + }}, + # Checkmark + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Checkmark", "parent": "Background" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Checkmark", "component_type": "Image" + }}, + # Label + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Label", "parent": "SoundToggle" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Label", "component_type": "TextMeshProUGUI" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "Label", + "component_type": "TextMeshProUGUI", + "properties": {"text": "Sound Effects", "fontSize": 18, "alignment": 513} + }} +]) +``` + +### Add Layout Group (Vertical/Horizontal/Grid) + +Layout groups auto-arrange child elements. Add to any container. + +```python +# Vertical layout for a menu panel +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_components", "params": { + "action": "add", "target": "MenuPanel", + "component_type": "VerticalLayoutGroup" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "MenuPanel", + "component_type": "VerticalLayoutGroup", + "properties": { + "spacing": 10, + "childAlignment": 1, + "childForceExpandWidth": True, + "childForceExpandHeight": False + } + }}, + # Add ContentSizeFitter to auto-resize + {"tool": "manage_components", "params": { + "action": "add", "target": "MenuPanel", + "component_type": "ContentSizeFitter" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "MenuPanel", + "component_type": "ContentSizeFitter", + "properties": { + "verticalFit": 2 + } + }} +]) +``` + +> **childAlignment values:** 0=UpperLeft, 1=UpperCenter, 2=UpperRight, 3=MiddleLeft, 4=MiddleCenter, 5=MiddleRight, 6=LowerLeft, 7=LowerCenter, 8=LowerRight. +> **ContentSizeFitter fit modes:** 0=Unconstrained, 1=MinSize, 2=PreferredSize. + +### Complete Example: Main Menu Screen + +Combines multiple templates into a full menu screen in two batch calls (25 command limit per batch). + +```python +# Batch 1: Canvas + EventSystem + Panel + Title +batch_execute(fail_fast=True, commands=[ + # Canvas + {"tool": "manage_gameobject", "params": {"action": "create", "name": "MenuCanvas"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "MenuCanvas", "component_type": "Canvas"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "MenuCanvas", "component_type": "CanvasScaler"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "MenuCanvas", "component_type": "GraphicRaycaster"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "MenuCanvas", "component_type": "Canvas", "property": "renderMode", "value": 0}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "MenuCanvas", "component_type": "CanvasScaler", "properties": {"uiScaleMode": 1, "referenceResolution": [1920, 1080]}}}, + # EventSystem + {"tool": "manage_gameobject", "params": {"action": "create", "name": "EventSystem"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "EventSystem", "component_type": "UnityEngine.EventSystems.EventSystem"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "EventSystem", "component_type": "UnityEngine.EventSystems.StandaloneInputModule"}}, + # Panel + {"tool": "manage_gameobject", "params": {"action": "create", "name": "MenuPanel", "parent": "MenuCanvas"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "MenuPanel", "component_type": "Image"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "MenuPanel", "component_type": "Image", "property": "color", "value": [0.1, 0.1, 0.15, 0.9]}}, + {"tool": "manage_components", "params": {"action": "add", "target": "MenuPanel", "component_type": "VerticalLayoutGroup"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "MenuPanel", "component_type": "VerticalLayoutGroup", "properties": {"spacing": 20, "childAlignment": 4, "childForceExpandWidth": True, "childForceExpandHeight": False}}}, + # Title + {"tool": "manage_gameobject", "params": {"action": "create", "name": "Title", "parent": "MenuPanel"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "Title", "component_type": "TextMeshProUGUI"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "Title", "component_type": "TextMeshProUGUI", "properties": {"text": "My Game", "fontSize": 64, "alignment": 514, "color": [1, 1, 1, 1]}}} +]) + +# Batch 2: Buttons +batch_execute(fail_fast=True, commands=[ + # Play Button + {"tool": "manage_gameobject", "params": {"action": "create", "name": "PlayButton", "parent": "MenuPanel"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "PlayButton", "component_type": "Image"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "PlayButton", "component_type": "Button"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "PlayButton", "component_type": "Image", "property": "color", "value": [0.2, 0.6, 1.0, 1.0]}}, + {"tool": "manage_gameobject", "params": {"action": "create", "name": "PlayButton_Label", "parent": "PlayButton"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "PlayButton_Label", "component_type": "TextMeshProUGUI"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "PlayButton_Label", "component_type": "TextMeshProUGUI", "properties": {"text": "Play", "fontSize": 32, "alignment": 514}}}, + # Settings Button + {"tool": "manage_gameobject", "params": {"action": "create", "name": "SettingsButton", "parent": "MenuPanel"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "SettingsButton", "component_type": "Image"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "SettingsButton", "component_type": "Button"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "SettingsButton", "component_type": "Image", "property": "color", "value": [0.3, 0.3, 0.35, 1.0]}}, + {"tool": "manage_gameobject", "params": {"action": "create", "name": "SettingsButton_Label", "parent": "SettingsButton"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "SettingsButton_Label", "component_type": "TextMeshProUGUI"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "SettingsButton_Label", "component_type": "TextMeshProUGUI", "properties": {"text": "Settings", "fontSize": 32, "alignment": 514}}}, + # Quit Button + {"tool": "manage_gameobject", "params": {"action": "create", "name": "QuitButton", "parent": "MenuPanel"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "QuitButton", "component_type": "Image"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "QuitButton", "component_type": "Button"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "QuitButton", "component_type": "Image", "property": "color", "value": [0.8, 0.2, 0.2, 1.0]}}, + {"tool": "manage_gameobject", "params": {"action": "create", "name": "QuitButton_Label", "parent": "QuitButton"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "QuitButton_Label", "component_type": "TextMeshProUGUI"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "QuitButton_Label", "component_type": "TextMeshProUGUI", "properties": {"text": "Quit", "fontSize": 32, "alignment": 514}}} +]) +``` + +### UI Component Quick Reference + +| UI Element | Required Components | Notes | +| ---------- | ------------------- | ----- | +| **Canvas** | Canvas + CanvasScaler + GraphicRaycaster | Root for all UI. One per screen. | +| **EventSystem** | EventSystem + StandaloneInputModule (or InputSystemUIInputModule) | One per scene. Required for interaction. | +| **Panel** | Image | Container. Set color for background. | +| **Text** | TextMeshProUGUI | Auto-adds RectTransform under Canvas. | +| **Button** | Image + Button + child(TextMeshProUGUI) | Image = visual, Button = click handler. | +| **Image** | Image | Set sprite property for custom graphics. | +| **Slider** | Slider + Image + children(Background, Fill Area/Fill, Handle Slide Area/Handle) | Complex hierarchy. | +| **Toggle** | Toggle + children(Background/Checkmark, Label) | Checkbox/radio button. | +| **Input Field** | Image + TMP_InputField + children(Text Area/Placeholder/Text) | Text input. | +| **Scroll View** | ScrollRect + Image + children(Viewport/Content, Scrollbar) | Scrollable container. | +| **Dropdown** | Image + TMP_Dropdown + children(Label, Arrow, Template) | Selection menu. | +| **Layout Group** | VerticalLayoutGroup / HorizontalLayoutGroup / GridLayoutGroup | Add to any container to auto-arrange children. | + +--- + ## Batch Operations ### Mass Property Update diff --git a/unity-mcp-skill/SKILL.md b/unity-mcp-skill/SKILL.md index 40a8541d8..b9a76357e 100644 --- a/unity-mcp-skill/SKILL.md +++ b/unity-mcp-skill/SKILL.md @@ -124,6 +124,7 @@ uri="file:///full/path/to/file.cs" | **Editor** | `manage_editor`, `execute_menu_item`, `read_console` | Editor control | | **Testing** | `run_tests`, `get_test_job` | Unity Test Framework | | **Batch** | `batch_execute` | Parallel/bulk operations | +| **UI** | `batch_execute` with `manage_gameobject` + `manage_components` | Canvas, Panel, Button, Text, Slider, Toggle, Input Field (see [UI workflows](references/workflows.md#ui-creation-workflows)) | ## Common Workflows diff --git a/unity-mcp-skill/references/workflows.md b/unity-mcp-skill/references/workflows.md index f04245cf4..629becfa8 100644 --- a/unity-mcp-skill/references/workflows.md +++ b/unity-mcp-skill/references/workflows.md @@ -10,6 +10,7 @@ Common workflows and patterns for effective Unity-MCP usage. - [Asset Management Workflows](#asset-management-workflows) - [Testing Workflows](#testing-workflows) - [Debugging Workflows](#debugging-workflows) +- [UI Creation Workflows](#ui-creation-workflows) - [Batch Operations](#batch-operations) --- @@ -491,6 +492,412 @@ manage_scene(action="screenshot") --- +## UI Creation Workflows + +Unity UI (Canvas-based UGUI) requires specific component hierarchies. Use `batch_execute` with `fail_fast=True` to create complete UI elements in a single call. These templates handle the boilerplate so you don't need to remember which components each UI element needs. + +### Create Canvas (Foundation for All UI) + +Every UI element must be under a Canvas. A Canvas requires three components: `Canvas`, `CanvasScaler`, and `GraphicRaycaster`. + +```python +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "MainCanvas" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "MainCanvas", "component_type": "Canvas" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "MainCanvas", "component_type": "CanvasScaler" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "MainCanvas", "component_type": "GraphicRaycaster" + }}, + # renderMode: 0=ScreenSpaceOverlay, 1=ScreenSpaceCamera, 2=WorldSpace + {"tool": "manage_components", "params": { + "action": "set_property", "target": "MainCanvas", + "component_type": "Canvas", "property": "renderMode", "value": 0 + }}, + # CanvasScaler: uiScaleMode 1=ScaleWithScreenSize + {"tool": "manage_components", "params": { + "action": "set_property", "target": "MainCanvas", + "component_type": "CanvasScaler", "property": "uiScaleMode", "value": 1 + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "MainCanvas", + "component_type": "CanvasScaler", "property": "referenceResolution", + "value": [1920, 1080] + }} +]) +``` + +### Create EventSystem (Required Once Per Scene for UI Interaction) + +If no EventSystem exists in the scene, buttons and other interactive UI elements won't respond to input. Create one alongside your first Canvas. + +```python +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "EventSystem" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "EventSystem", + "component_type": "UnityEngine.EventSystems.EventSystem" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "EventSystem", + "component_type": "UnityEngine.InputSystem.UI.InputSystemUIInputModule" + }} +]) +``` + +> **Note:** For projects using legacy Input Manager instead of Input System, use `"component_type": "UnityEngine.EventSystems.StandaloneInputModule"` instead. + +### Create Panel (Background Container) + +A Panel is an Image component used as a background/container for other UI elements. + +```python +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "MenuPanel", "parent": "MainCanvas" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "MenuPanel", "component_type": "Image" + }}, + # Set semi-transparent dark background + {"tool": "manage_components", "params": { + "action": "set_property", "target": "MenuPanel", + "component_type": "Image", "property": "color", + "value": [0.1, 0.1, 0.1, 0.8] + }} +]) +``` + +### Create Text (TextMeshPro) + +TextMeshProUGUI automatically adds a RectTransform when added to a child of a Canvas. + +```python +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "TitleText", "parent": "MenuPanel" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "TitleText", + "component_type": "TextMeshProUGUI" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "TitleText", + "component_type": "TextMeshProUGUI", + "properties": { + "text": "My Game Title", + "fontSize": 48, + "alignment": 514, + "color": [1, 1, 1, 1] + } + }} +]) +``` + +> **TextMeshPro alignment values:** 257=TopLeft, 258=TopCenter, 260=TopRight, 513=MiddleLeft, 514=MiddleCenter, 516=MiddleRight, 1025=BottomLeft, 1026=BottomCenter, 1028=BottomRight. + +### Create Button (With Label) + +A Button needs an `Image` (visual) + `Button` (interaction) on the parent, and a child with `TextMeshProUGUI` for the label. + +```python +batch_execute(fail_fast=True, commands=[ + # Button container with Image + Button components + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "StartButton", "parent": "MenuPanel" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "StartButton", "component_type": "Image" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "StartButton", "component_type": "Button" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "StartButton", + "component_type": "Image", "property": "color", + "value": [0.2, 0.6, 1.0, 1.0] + }}, + # Child text label + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "StartButton_Label", "parent": "StartButton" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "StartButton_Label", + "component_type": "TextMeshProUGUI" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "StartButton_Label", + "component_type": "TextMeshProUGUI", + "properties": {"text": "Start Game", "fontSize": 24, "alignment": 514} + }} +]) +``` + +### Create Slider + +A Slider requires a specific hierarchy: the slider root, a background, a fill area with fill, and a handle area with handle. + +```python +batch_execute(fail_fast=True, commands=[ + # Slider root + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "HealthSlider", "parent": "MainCanvas" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "HealthSlider", "component_type": "Slider" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "HealthSlider", "component_type": "Image" + }}, + # Background + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Background", "parent": "HealthSlider" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Background", "component_type": "Image" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "Background", + "component_type": "Image", "property": "color", + "value": [0.3, 0.3, 0.3, 1.0] + }}, + # Fill Area + Fill + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Fill Area", "parent": "HealthSlider" + }}, + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Fill", "parent": "Fill Area" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Fill", "component_type": "Image" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "Fill", + "component_type": "Image", "property": "color", + "value": [0.2, 0.8, 0.2, 1.0] + }}, + # Handle Area + Handle + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Handle Slide Area", "parent": "HealthSlider" + }}, + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Handle", "parent": "Handle Slide Area" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Handle", "component_type": "Image" + }} +]) +``` + +### Create Input Field (TextMeshPro) + +```python +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "NameInput", "parent": "MenuPanel" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "NameInput", "component_type": "Image" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "NameInput", + "component_type": "TMP_InputField" + }}, + # Text area child + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Text Area", "parent": "NameInput" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Text Area", + "component_type": "RectMask2D" + }}, + # Placeholder + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Placeholder", "parent": "Text Area" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Placeholder", + "component_type": "TextMeshProUGUI" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "Placeholder", + "component_type": "TextMeshProUGUI", + "properties": {"text": "Enter name...", "fontStyle": 2, "color": [0.5, 0.5, 0.5, 0.5]} + }}, + # Actual text + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Text", "parent": "Text Area" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Text", + "component_type": "TextMeshProUGUI" + }} +]) +``` + +### Create Toggle (Checkbox) + +```python +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "SoundToggle", "parent": "MenuPanel" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "SoundToggle", "component_type": "Toggle" + }}, + # Background box + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Background", "parent": "SoundToggle" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Background", "component_type": "Image" + }}, + # Checkmark + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Checkmark", "parent": "Background" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Checkmark", "component_type": "Image" + }}, + # Label + {"tool": "manage_gameobject", "params": { + "action": "create", "name": "Label", "parent": "SoundToggle" + }}, + {"tool": "manage_components", "params": { + "action": "add", "target": "Label", "component_type": "TextMeshProUGUI" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "Label", + "component_type": "TextMeshProUGUI", + "properties": {"text": "Sound Effects", "fontSize": 18, "alignment": 513} + }} +]) +``` + +### Add Layout Group (Vertical/Horizontal/Grid) + +Layout groups auto-arrange child elements. Add to any container. + +```python +# Vertical layout for a menu panel +batch_execute(fail_fast=True, commands=[ + {"tool": "manage_components", "params": { + "action": "add", "target": "MenuPanel", + "component_type": "VerticalLayoutGroup" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "MenuPanel", + "component_type": "VerticalLayoutGroup", + "properties": { + "spacing": 10, + "childAlignment": 1, + "childForceExpandWidth": True, + "childForceExpandHeight": False + } + }}, + # Add ContentSizeFitter to auto-resize + {"tool": "manage_components", "params": { + "action": "add", "target": "MenuPanel", + "component_type": "ContentSizeFitter" + }}, + {"tool": "manage_components", "params": { + "action": "set_property", "target": "MenuPanel", + "component_type": "ContentSizeFitter", + "properties": { + "verticalFit": 2 + } + }} +]) +``` + +> **childAlignment values:** 0=UpperLeft, 1=UpperCenter, 2=UpperRight, 3=MiddleLeft, 4=MiddleCenter, 5=MiddleRight, 6=LowerLeft, 7=LowerCenter, 8=LowerRight. +> **ContentSizeFitter fit modes:** 0=Unconstrained, 1=MinSize, 2=PreferredSize. + +### Complete Example: Main Menu Screen + +Combines multiple templates into a full menu screen in two batch calls (25 command limit per batch). + +```python +# Batch 1: Canvas + EventSystem + Panel + Title +batch_execute(fail_fast=True, commands=[ + # Canvas + {"tool": "manage_gameobject", "params": {"action": "create", "name": "MenuCanvas"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "MenuCanvas", "component_type": "Canvas"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "MenuCanvas", "component_type": "CanvasScaler"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "MenuCanvas", "component_type": "GraphicRaycaster"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "MenuCanvas", "component_type": "Canvas", "property": "renderMode", "value": 0}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "MenuCanvas", "component_type": "CanvasScaler", "properties": {"uiScaleMode": 1, "referenceResolution": [1920, 1080]}}}, + # EventSystem + {"tool": "manage_gameobject", "params": {"action": "create", "name": "EventSystem"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "EventSystem", "component_type": "UnityEngine.EventSystems.EventSystem"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "EventSystem", "component_type": "UnityEngine.EventSystems.StandaloneInputModule"}}, + # Panel + {"tool": "manage_gameobject", "params": {"action": "create", "name": "MenuPanel", "parent": "MenuCanvas"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "MenuPanel", "component_type": "Image"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "MenuPanel", "component_type": "Image", "property": "color", "value": [0.1, 0.1, 0.15, 0.9]}}, + {"tool": "manage_components", "params": {"action": "add", "target": "MenuPanel", "component_type": "VerticalLayoutGroup"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "MenuPanel", "component_type": "VerticalLayoutGroup", "properties": {"spacing": 20, "childAlignment": 4, "childForceExpandWidth": True, "childForceExpandHeight": False}}}, + # Title + {"tool": "manage_gameobject", "params": {"action": "create", "name": "Title", "parent": "MenuPanel"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "Title", "component_type": "TextMeshProUGUI"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "Title", "component_type": "TextMeshProUGUI", "properties": {"text": "My Game", "fontSize": 64, "alignment": 514, "color": [1, 1, 1, 1]}}} +]) + +# Batch 2: Buttons +batch_execute(fail_fast=True, commands=[ + # Play Button + {"tool": "manage_gameobject", "params": {"action": "create", "name": "PlayButton", "parent": "MenuPanel"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "PlayButton", "component_type": "Image"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "PlayButton", "component_type": "Button"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "PlayButton", "component_type": "Image", "property": "color", "value": [0.2, 0.6, 1.0, 1.0]}}, + {"tool": "manage_gameobject", "params": {"action": "create", "name": "PlayButton_Label", "parent": "PlayButton"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "PlayButton_Label", "component_type": "TextMeshProUGUI"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "PlayButton_Label", "component_type": "TextMeshProUGUI", "properties": {"text": "Play", "fontSize": 32, "alignment": 514}}}, + # Settings Button + {"tool": "manage_gameobject", "params": {"action": "create", "name": "SettingsButton", "parent": "MenuPanel"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "SettingsButton", "component_type": "Image"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "SettingsButton", "component_type": "Button"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "SettingsButton", "component_type": "Image", "property": "color", "value": [0.3, 0.3, 0.35, 1.0]}}, + {"tool": "manage_gameobject", "params": {"action": "create", "name": "SettingsButton_Label", "parent": "SettingsButton"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "SettingsButton_Label", "component_type": "TextMeshProUGUI"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "SettingsButton_Label", "component_type": "TextMeshProUGUI", "properties": {"text": "Settings", "fontSize": 32, "alignment": 514}}}, + # Quit Button + {"tool": "manage_gameobject", "params": {"action": "create", "name": "QuitButton", "parent": "MenuPanel"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "QuitButton", "component_type": "Image"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "QuitButton", "component_type": "Button"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "QuitButton", "component_type": "Image", "property": "color", "value": [0.8, 0.2, 0.2, 1.0]}}, + {"tool": "manage_gameobject", "params": {"action": "create", "name": "QuitButton_Label", "parent": "QuitButton"}}, + {"tool": "manage_components", "params": {"action": "add", "target": "QuitButton_Label", "component_type": "TextMeshProUGUI"}}, + {"tool": "manage_components", "params": {"action": "set_property", "target": "QuitButton_Label", "component_type": "TextMeshProUGUI", "properties": {"text": "Quit", "fontSize": 32, "alignment": 514}}} +]) +``` + +### UI Component Quick Reference + +| UI Element | Required Components | Notes | +| ---------- | ------------------- | ----- | +| **Canvas** | Canvas + CanvasScaler + GraphicRaycaster | Root for all UI. One per screen. | +| **EventSystem** | EventSystem + StandaloneInputModule (or InputSystemUIInputModule) | One per scene. Required for interaction. | +| **Panel** | Image | Container. Set color for background. | +| **Text** | TextMeshProUGUI | Auto-adds RectTransform under Canvas. | +| **Button** | Image + Button + child(TextMeshProUGUI) | Image = visual, Button = click handler. | +| **Image** | Image | Set sprite property for custom graphics. | +| **Slider** | Slider + Image + children(Background, Fill Area/Fill, Handle Slide Area/Handle) | Complex hierarchy. | +| **Toggle** | Toggle + children(Background/Checkmark, Label) | Checkbox/radio button. | +| **Input Field** | Image + TMP_InputField + children(Text Area/Placeholder/Text) | Text input. | +| **Scroll View** | ScrollRect + Image + children(Viewport/Content, Scrollbar) | Scrollable container. | +| **Dropdown** | Image + TMP_Dropdown + children(Label, Arrow, Template) | Selection menu. | +| **Layout Group** | VerticalLayoutGroup / HorizontalLayoutGroup / GridLayoutGroup | Add to any container to auto-arrange children. | + +--- + ## Batch Operations ### Mass Property Update From f067a10999f8163d06f4588846ea6b27aaacf8d8 Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Wed, 11 Feb 2026 18:46:39 -0500 Subject: [PATCH 2/2] [fix and mod] Bug fix and batch request customization 1. Fix the bug where for certain MacOS user (like me), the GitURLoverride is set to the unity-mcp folder rather than the unity-mcp/Server 2. Customize batch for it to take 0-100 requests. Tested on MacOS. --- .claude/skills/unity-mcp-skill/SKILL.md | 2 +- .../unity-mcp-skill/references/workflows.md | 2 +- .../Editor/Constants/EditorPrefKeys.cs | 2 + .../Editor/Helpers/AssetPathUtility.cs | 64 ++++++++++++++++++- .../Editor/Services/EditorStateCache.cs | 13 ++++ MCPForUnity/Editor/Tools/BatchExecute.cs | 23 ++++++- .../Components/Advanced/McpAdvancedSection.cs | 57 ++++++++++++++++- .../Components/Tools/McpToolsSection.cs | 53 +++++++++++++++ Server/src/services/resources/editor_state.py | 5 ++ Server/src/services/tools/batch_execute.py | 55 ++++++++++++++-- unity-mcp-skill/SKILL.md | 2 +- unity-mcp-skill/references/workflows.md | 2 +- 12 files changed, 267 insertions(+), 13 deletions(-) diff --git a/.claude/skills/unity-mcp-skill/SKILL.md b/.claude/skills/unity-mcp-skill/SKILL.md index b9a76357e..938ad19a6 100644 --- a/.claude/skills/unity-mcp-skill/SKILL.md +++ b/.claude/skills/unity-mcp-skill/SKILL.md @@ -45,7 +45,7 @@ batch_execute( ) ``` -**Max 25 commands per batch.** Use `fail_fast=True` for dependent operations. +**Max 25 commands per batch by default (configurable in Unity MCP Tools window, hard max 100).** Use `fail_fast=True` for dependent operations. ### 3. Use `screenshot` in manage_scene to Verify Visual Results diff --git a/.claude/skills/unity-mcp-skill/references/workflows.md b/.claude/skills/unity-mcp-skill/references/workflows.md index 629becfa8..511f6fccb 100644 --- a/.claude/skills/unity-mcp-skill/references/workflows.md +++ b/.claude/skills/unity-mcp-skill/references/workflows.md @@ -822,7 +822,7 @@ batch_execute(fail_fast=True, commands=[ ### Complete Example: Main Menu Screen -Combines multiple templates into a full menu screen in two batch calls (25 command limit per batch). +Combines multiple templates into a full menu screen in two batch calls (default 25 command limit per batch, configurable in Unity MCP Tools window up to 100). ```python # Batch 1: Canvas + EventSystem + Panel + Title diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index 5f99d3e5a..f1360984b 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -63,5 +63,7 @@ internal static class EditorPrefKeys internal const string CustomerUuid = "MCPForUnity.CustomerUUID"; internal const string ApiKey = "MCPForUnity.ApiKey"; + + internal const string BatchExecuteMaxCommands = "MCPForUnity.BatchExecute.MaxCommands"; } } diff --git a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs index fc450bada..c45469413 100644 --- a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs +++ b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs @@ -201,6 +201,8 @@ public static JObject GetPackageJson() /// Gets the package source for the MCP server (used with uvx --from). /// Checks for EditorPrefs override first (supports git URLs, file:// paths, etc.), /// then falls back to PyPI package reference. + /// When the override is a local path, auto-corrects to the "Server" subdirectory + /// if the path doesn't contain pyproject.toml but Server/pyproject.toml exists. /// /// Package source string for uvx --from argument public static string GetMcpServerPackageSource() @@ -209,7 +211,14 @@ public static string GetMcpServerPackageSource() string sourceOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, ""); if (!string.IsNullOrEmpty(sourceOverride)) { - return sourceOverride; + string resolved = ResolveLocalServerPath(sourceOverride); + // Persist the corrected path so future reads are consistent + if (resolved != sourceOverride) + { + EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, resolved); + McpLog.Info($"Auto-corrected server source override from '{sourceOverride}' to '{resolved}'"); + } + return resolved; } // Default to PyPI package (avoids Windows long path issues with git clone) @@ -223,6 +232,59 @@ public static string GetMcpServerPackageSource() return $"mcpforunityserver=={version}"; } + /// + /// Validates and auto-corrects a local server source path to ensure it points to the + /// directory containing pyproject.toml. If the path points to a parent directory + /// (e.g. the repo root "unity-mcp") instead of the Python package directory ("Server"), + /// this checks for a "Server" subdirectory with pyproject.toml and returns that path. + /// Non-local paths (URLs, PyPI references) are returned unchanged. + /// + internal static string ResolveLocalServerPath(string path) + { + if (string.IsNullOrEmpty(path)) + return path; + + // Skip non-local paths (git URLs, PyPI package names, etc.) + if (path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("git+", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("ssh://", StringComparison.OrdinalIgnoreCase)) + { + return path; + } + + // If it looks like a PyPI package reference (no path separators), skip + if (!path.Contains('/') && !path.Contains('\\') && !path.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) + { + return path; + } + + // Strip file:// prefix for filesystem checks, preserve for return value + string checkPath = path; + string prefix = string.Empty; + if (checkPath.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) + { + prefix = checkPath.Substring(0, 7); // preserve original casing + checkPath = checkPath.Substring(7); + } + + // Already correct — pyproject.toml exists at this path + if (System.IO.File.Exists(System.IO.Path.Combine(checkPath, "pyproject.toml"))) + { + return path; + } + + // Check if "Server" subdirectory contains pyproject.toml + string serverSubDir = System.IO.Path.Combine(checkPath, "Server"); + if (System.IO.File.Exists(System.IO.Path.Combine(serverSubDir, "pyproject.toml"))) + { + return prefix + serverSubDir; + } + + // Return as-is; uvx will report the error if the path is truly invalid + return path; + } + /// /// Deprecated: Use GetMcpServerPackageSource() instead. /// Kept for backwards compatibility. diff --git a/MCPForUnity/Editor/Services/EditorStateCache.cs b/MCPForUnity/Editor/Services/EditorStateCache.cs index 24fec0f14..9dd85d529 100644 --- a/MCPForUnity/Editor/Services/EditorStateCache.cs +++ b/MCPForUnity/Editor/Services/EditorStateCache.cs @@ -75,6 +75,9 @@ private sealed class EditorStateSnapshot [JsonProperty("transport")] public EditorStateTransport Transport { get; set; } + + [JsonProperty("settings")] + public EditorStateSettings Settings { get; set; } } private sealed class EditorStateUnity @@ -239,6 +242,12 @@ private sealed class EditorStateTransport public long? LastMessageUnixMs { get; set; } } + private sealed class EditorStateSettings + { + [JsonProperty("batch_execute_max_commands")] + public int BatchExecuteMaxCommands { get; set; } + } + static EditorStateCache() { try @@ -482,6 +491,10 @@ private static JObject BuildSnapshot(string reason) { UnityBridgeConnected = null, LastMessageUnixMs = null + }, + Settings = new EditorStateSettings + { + BatchExecuteMaxCommands = Tools.BatchExecute.GetMaxCommandsPerBatch() } }; diff --git a/MCPForUnity/Editor/Tools/BatchExecute.cs b/MCPForUnity/Editor/Tools/BatchExecute.cs index d9df336d6..66dc2b39b 100644 --- a/MCPForUnity/Editor/Tools/BatchExecute.cs +++ b/MCPForUnity/Editor/Tools/BatchExecute.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using MCPForUnity.Editor.Constants; using MCPForUnity.Editor.Helpers; using Newtonsoft.Json.Linq; +using UnityEditor; namespace MCPForUnity.Editor.Tools { @@ -13,7 +15,20 @@ namespace MCPForUnity.Editor.Tools [McpForUnityTool("batch_execute", AutoRegister = false)] public static class BatchExecute { - private const int MaxCommandsPerBatch = 25; + /// Default limit when no EditorPrefs override is set. + internal const int DefaultMaxCommandsPerBatch = 25; + + /// Hard ceiling to prevent extreme editor freezes regardless of user setting. + internal const int AbsoluteMaxCommandsPerBatch = 100; + + /// + /// Returns the user-configured max commands per batch, clamped between 1 and . + /// + internal static int GetMaxCommandsPerBatch() + { + int configured = EditorPrefs.GetInt(EditorPrefKeys.BatchExecuteMaxCommands, DefaultMaxCommandsPerBatch); + return Math.Clamp(configured, 1, AbsoluteMaxCommandsPerBatch); + } public static async Task HandleCommand(JObject @params) { @@ -28,9 +43,11 @@ public static async Task HandleCommand(JObject @params) return new ErrorResponse("Provide at least one command entry in 'commands'."); } - if (commandsToken.Count > MaxCommandsPerBatch) + int maxCommands = GetMaxCommandsPerBatch(); + if (commandsToken.Count > maxCommands) { - return new ErrorResponse($"A maximum of {MaxCommandsPerBatch} commands are allowed per batch."); + return new ErrorResponse( + $"A maximum of {maxCommands} commands are allowed per batch (configurable in MCP Tools window, hard max {AbsoluteMaxCommandsPerBatch})."); } bool failFast = @params.Value("failFast") ?? false; diff --git a/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs b/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs index 4957e7da8..4ec8b07f8 100644 --- a/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs @@ -158,6 +158,12 @@ private void RegisterCallbacks() } else { + url = ResolveServerPath(url); + // Update the text field if the path was auto-corrected, without re-triggering the callback + if (url != evt.newValue?.Trim()) + { + gitUrlOverride.SetValueWithoutNotify(url); + } EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, url); } OnGitUrlChanged?.Invoke(); @@ -326,9 +332,10 @@ private void OnClearUvxClicked() private void OnBrowseGitUrlClicked() { - string picked = EditorUtility.OpenFolderPanel("Select Server folder", string.Empty, string.Empty); + string picked = EditorUtility.OpenFolderPanel("Select Server folder (containing pyproject.toml)", string.Empty, string.Empty); if (!string.IsNullOrEmpty(picked)) { + picked = ResolveServerPath(picked); gitUrlOverride.value = picked; EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, picked); OnGitUrlChanged?.Invoke(); @@ -337,6 +344,54 @@ private void OnBrowseGitUrlClicked() } } + /// + /// Validates and auto-corrects a local server path to ensure it points to the directory + /// containing pyproject.toml (the Python package root). If the user selects a parent + /// directory (e.g. the repo root), this checks for a "Server" subdirectory with + /// pyproject.toml and returns that instead. + /// + private static string ResolveServerPath(string path) + { + if (string.IsNullOrEmpty(path)) + return path; + + // If path is not a local filesystem path, return as-is (git URLs, PyPI refs, etc.) + if (path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("git+", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("ssh://", StringComparison.OrdinalIgnoreCase)) + { + return path; + } + + // Strip file:// prefix for filesystem checks, but preserve it for the return value + string checkPath = path; + string prefix = string.Empty; + if (checkPath.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) + { + prefix = "file://"; + checkPath = checkPath.Substring(7); + } + + // Already points to a directory with pyproject.toml — correct path + if (File.Exists(Path.Combine(checkPath, "pyproject.toml"))) + { + return path; + } + + // Check if "Server" subdirectory contains pyproject.toml (common repo structure) + string serverSubDir = Path.Combine(checkPath, "Server"); + if (File.Exists(Path.Combine(serverSubDir, "pyproject.toml"))) + { + string corrected = prefix + serverSubDir; + McpLog.Info($"Auto-corrected server path to 'Server' subdirectory: {corrected}"); + return corrected; + } + + // Return as-is; uvx will report the error if the path is invalid + return path; + } + private void UpdateDeploymentSection() { var deployService = MCPServiceLocator.Deployment; diff --git a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs index dc5a3eabf..21513b5db 100644 --- a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs @@ -220,6 +220,11 @@ private VisualElement CreateToolRow(ToolMetadata tool) row.Add(CreateManageSceneActions()); } + if (IsBatchExecuteTool(tool)) + { + row.Add(CreateBatchExecuteSettings()); + } + return row; } @@ -296,6 +301,52 @@ private VisualElement CreateManageSceneActions() return actions; } + private VisualElement CreateBatchExecuteSettings() + { + var container = new VisualElement(); + container.AddToClassList("tool-item-actions"); + container.style.flexDirection = FlexDirection.Row; + container.style.alignItems = Align.Center; + container.style.marginTop = 4; + + var label = new Label("Max commands per batch:"); + label.style.marginRight = 8; + label.style.unityFontStyleAndWeight = UnityEngine.FontStyle.Normal; + container.Add(label); + + int currentValue = EditorPrefs.GetInt( + EditorPrefKeys.BatchExecuteMaxCommands, + BatchExecute.DefaultMaxCommandsPerBatch + ); + + var field = new IntegerField + { + value = Math.Clamp(currentValue, 1, BatchExecute.AbsoluteMaxCommandsPerBatch), + style = { width = 60 } + }; + field.tooltip = $"Number of commands allowed per batch_execute call (1–{BatchExecute.AbsoluteMaxCommandsPerBatch}). Default: {BatchExecute.DefaultMaxCommandsPerBatch}."; + + field.RegisterValueChangedCallback(evt => + { + int clamped = Math.Clamp(evt.newValue, 1, BatchExecute.AbsoluteMaxCommandsPerBatch); + if (clamped != evt.newValue) + { + field.SetValueWithoutNotify(clamped); + } + EditorPrefs.SetInt(EditorPrefKeys.BatchExecuteMaxCommands, clamped); + }); + + container.Add(field); + + var hint = new Label($"(max {BatchExecute.AbsoluteMaxCommandsPerBatch})"); + hint.style.marginLeft = 4; + hint.style.color = new UnityEngine.Color(0.5f, 0.5f, 0.5f); + hint.style.fontSize = 10; + container.Add(hint); + + return container; + } + private void OnManageSceneScreenshotClicked() { try @@ -329,6 +380,8 @@ private static Label CreateTag(string text) private static bool IsManageSceneTool(ToolMetadata tool) => string.Equals(tool?.Name, "manage_scene", StringComparison.OrdinalIgnoreCase); + private static bool IsBatchExecuteTool(ToolMetadata tool) => string.Equals(tool?.Name, "batch_execute", StringComparison.OrdinalIgnoreCase); + private static bool IsBuiltIn(ToolMetadata tool) => tool?.IsBuiltIn ?? false; } } diff --git a/Server/src/services/resources/editor_state.py b/Server/src/services/resources/editor_state.py index 4de79429e..f14146fac 100644 --- a/Server/src/services/resources/editor_state.py +++ b/Server/src/services/resources/editor_state.py @@ -91,6 +91,10 @@ class EditorStateTransport(BaseModel): last_message_unix_ms: int | None = None +class EditorStateSettings(BaseModel): + batch_execute_max_commands: int | None = None + + class EditorStateAdvice(BaseModel): ready_for_tools: bool | None = None blocking_reasons: list[str] | None = None @@ -114,6 +118,7 @@ class EditorStateData(BaseModel): assets: EditorStateAssets | None = None tests: EditorStateTests | None = None transport: EditorStateTransport | None = None + settings: EditorStateSettings | None = None advice: EditorStateAdvice | None = None staleness: EditorStateStaleness | None = None diff --git a/Server/src/services/tools/batch_execute.py b/Server/src/services/tools/batch_execute.py index bd480e3d1..4e41da364 100644 --- a/Server/src/services/tools/batch_execute.py +++ b/Server/src/services/tools/batch_execute.py @@ -1,6 +1,7 @@ """Defines the batch_execute tool for orchestrating multiple Unity MCP commands.""" from __future__ import annotations +import logging from typing import Annotated, Any from fastmcp import Context @@ -11,7 +12,51 @@ from transport.unity_transport import send_with_unity_instance from transport.legacy.unity_connection import async_send_command_with_retry -MAX_COMMANDS_PER_BATCH = 25 +logger = logging.getLogger(__name__) + +# Fallback used when the Unity-side configured limit is not yet known. +DEFAULT_MAX_COMMANDS_PER_BATCH = 25 + +# Hard ceiling matching the C# AbsoluteMaxCommandsPerBatch. +ABSOLUTE_MAX_COMMANDS_PER_BATCH = 100 + +# Module-level cache for the Unity-configured limit (populated from editor state). +_cached_max_commands: int | None = None + + +async def _get_max_commands_from_editor_state(ctx: Context) -> int: + """ + Attempt to read the configured batch limit from the Unity editor state. + Falls back to DEFAULT_MAX_COMMANDS_PER_BATCH if unavailable. + """ + global _cached_max_commands + if _cached_max_commands is not None: + return _cached_max_commands + + try: + from services.resources.editor_state import get_editor_state + + state_resp = await get_editor_state(ctx) + data = state_resp.data if hasattr(state_resp, "data") else ( + state_resp.get("data") if isinstance(state_resp, dict) else None + ) + if isinstance(data, dict): + settings = data.get("settings") + if isinstance(settings, dict): + limit = settings.get("batch_execute_max_commands") + if isinstance(limit, int) and 1 <= limit <= ABSOLUTE_MAX_COMMANDS_PER_BATCH: + _cached_max_commands = limit + return limit + except Exception as exc: + logger.debug("Could not read batch limit from editor state: %s", exc) + + return DEFAULT_MAX_COMMANDS_PER_BATCH + + +def invalidate_cached_max_commands() -> None: + """Reset the cached limit so the next call re-reads from editor state.""" + global _cached_max_commands + _cached_max_commands = None @mcp_for_unity_tool( @@ -20,7 +65,8 @@ "Executes multiple MCP commands in a single batch for dramatically better performance. " "STRONGLY RECOMMENDED when creating/modifying multiple objects, adding components to multiple targets, " "or performing any repetitive operations. Reduces latency and token costs by 10-100x compared to " - "sequential tool calls. Supports up to 25 commands per batch. " + "sequential tool calls. The max commands per batch is configurable in the Unity MCP Tools window " + f"(default {DEFAULT_MAX_COMMANDS_PER_BATCH}, hard max {ABSOLUTE_MAX_COMMANDS_PER_BATCH}). " "Example: creating 5 cubes → use 1 batch_execute with 5 create commands instead of 5 separate calls." ), annotations=ToolAnnotations( @@ -45,9 +91,10 @@ async def batch_execute( raise ValueError( "'commands' must be a non-empty list of command specifications") - if len(commands) > MAX_COMMANDS_PER_BATCH: + max_commands = await _get_max_commands_from_editor_state(ctx) + if len(commands) > max_commands: raise ValueError( - f"batch_execute currently supports up to {MAX_COMMANDS_PER_BATCH} commands; received {len(commands)}" + f"batch_execute supports up to {max_commands} commands (configured in Unity); received {len(commands)}" ) normalized_commands: list[dict[str, Any]] = [] diff --git a/unity-mcp-skill/SKILL.md b/unity-mcp-skill/SKILL.md index b9a76357e..938ad19a6 100644 --- a/unity-mcp-skill/SKILL.md +++ b/unity-mcp-skill/SKILL.md @@ -45,7 +45,7 @@ batch_execute( ) ``` -**Max 25 commands per batch.** Use `fail_fast=True` for dependent operations. +**Max 25 commands per batch by default (configurable in Unity MCP Tools window, hard max 100).** Use `fail_fast=True` for dependent operations. ### 3. Use `screenshot` in manage_scene to Verify Visual Results diff --git a/unity-mcp-skill/references/workflows.md b/unity-mcp-skill/references/workflows.md index 629becfa8..511f6fccb 100644 --- a/unity-mcp-skill/references/workflows.md +++ b/unity-mcp-skill/references/workflows.md @@ -822,7 +822,7 @@ batch_execute(fail_fast=True, commands=[ ### Complete Example: Main Menu Screen -Combines multiple templates into a full menu screen in two batch calls (25 command limit per batch). +Combines multiple templates into a full menu screen in two batch calls (default 25 command limit per batch, configurable in Unity MCP Tools window up to 100). ```python # Batch 1: Canvas + EventSystem + Panel + Title