Skip to content

[Feature] New UI system#829

Merged
Scriptwonder merged 4 commits intoCoplayDev:betafrom
Scriptwonder:ui
Feb 26, 2026
Merged

[Feature] New UI system#829
Scriptwonder merged 4 commits intoCoplayDev:betafrom
Scriptwonder:ui

Conversation

@Scriptwonder
Copy link
Collaborator

@Scriptwonder Scriptwonder commented Feb 26, 2026

Description

This introduce a quicker way to initiate UI, as the uGUI way is proved to not be painful creation progress with the current LLM. Using a current tool, you can generate UIs based on UI toolkit directly and quickly modify them to your need.

Type of Change

Save your change type

  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Test update

Changes Made

  1. New manage_ui tool that generate based on UI toolkit
  2. Documentation update asking LLM to generate UI via UI toolkit

Testing/Screenshots/Recordings

image

(And uGui is generally not good for creating screen sized UI, since LLM has not overcome the spatial reasoning challenge yet)

截屏2026-02-25 下午9 19 48

Documentation Updates

  • I have added/removed/modified tools or resources
  • If yes, I have updated all documentation files using:
    • [] The LLM prompt at tools/UPDATE_DOCS_PROMPT.md (recommended)
    • Manual updates following the guide at tools/UPDATE_DOCS.md

Related Issues

Additional Notes

Summary by Sourcery

Introduce a new manage_ui tool and UI Toolkit–first workflows for Unity MCP, enabling creation, management, and visualization of UI Toolkit-based interfaces alongside existing uGUI support.

New Features:

  • Add the manage_ui MCP tool to create, read, update, delete, and list UI Toolkit UXML/USS assets and manage UIDocument attachments and visual trees.
  • Support rendering UI Toolkit panels to screenshots for visual inspection and self-evaluation workflows.
  • Expose manage_ui in the server tool layer with parameter validation, base64 content handling, and action routing for both read-only and mutating operations.

Enhancements:

  • Document UI Toolkit as the preferred UI system, including detailed manage_ui workflows and updated capability tables in skill and tools references.
  • Extend project_info with uiToolkit and screenCapture capability flags to better steer UI and screenshot behavior.
  • Update skill overviews and README files to list manage_ui as the primary UI tool and distinguish UI Toolkit from legacy uGUI workflows.

Tests:

  • Add Unity edit-mode tests covering manage_ui editor behaviors for file operations, PanelSettings management, UIDocument attachment/detachment, listing, and visual element modification.
  • Add Python integration tests for the manage_ui server wrapper, including path validation, content encoding, action routing, and parameter shaping for Unity commands.

Summary by CodeRabbit

  • New Features

    • Added manage_ui tool for UI Toolkit: create/read/update/delete UXML/USS, attach/detach UIDocuments, create PanelSettings, inspect visual trees, and render/capture UI. Project info now reports UI Toolkit and screen-capture availability.
  • Documentation

    • Updated docs to describe dual UI workflows (UI Toolkit preferred for Unity 2021+, and uGUI fallback) with examples and guidance.
  • Tests

    • Added integration and editor test suites covering manage_ui actions, validation, encoding, rendering, and error cases.

Add a number of UI management actions and helpers to the Unity editor tool and server interface. Implemented new editor actions: update_panel_settings, render_ui (editor + play-mode screenshot capture with RT lifecycle), link_stylesheet (insert Style src into UXML), delete (asset deletion), list (paginated UI asset listing), detach_ui_document, and modify_visual_element (change text, classes, inline styles, visibility, tooltip). Refactored PanelSettings creation to accept a generic 'settings' dict and introduced ApplyPanelSettingsProperties with parsing helpers for enums, colors, numeric and composite fields. Persist RenderTextures per PanelSettings for render_ui and added play-mode coroutine capture. Server-side tool metadata and routing updated to expose new actions and parameters; mutation routing updated accordingly. Added integration tests for render_ui and link_stylesheet and Unity edit-mode tests for delete. Misc: added using imports and various helper functions for path/file handling and inline style application.
Copilot AI review requested due to automatic review settings February 26, 2026 02:21
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a8e815 and 317ab83.

📒 Files selected for processing (2)
  • MCPForUnity/Editor/Tools/ManageUI.cs
  • Server/tests/integration/test_manage_ui.py

📝 Walkthrough

Walkthrough

Adds a new manage_ui tool with Unity Editor implementation and server-side wrapper to manage UI Toolkit assets (UXML/USS), PanelSettings, UIDocument wiring, visual-tree inspection, and optional render/capture; updates project info flags, docs/skill metadata, manifest, and adds extensive server + Unity tests.

Changes

Cohort / File(s) Summary
Editor Tool
MCPForUnity/Editor/Tools/ManageUI.cs, MCPForUnity/Editor/Tools/ManageUI.cs.meta
New static editor tool ManageUI exposing HandleCommand(JObject) for ping, CRUD on UXML/USS, attach/detach UIDocument, create/update PanelSettings, visual-tree export, stylesheet linking, visual-element modification, and render/capture pipeline with internal helpers and teardown hooks.
Server Tool
Server/src/services/tools/manage_ui.py
New Python manage_ui(...) tool that validates paths, base64-encodes/decodes contents, builds param payloads, routes read vs mutation commands to Unity, and normalizes responses across actions (create/read/update/delete, attach_ui_document, create_panel_settings, get_visual_tree, render_ui, link_stylesheet, list, modify_visual_element, ping).
Tests
Server/tests/integration/test_manage_ui.py, TestProjects/.../ManageUITests.cs, .../ManageUITests.cs.meta
New server integration tests and Unity editor tests covering path validation, encoding/decoding, routing, CRUD, PanelSettings, UIDocument attach/detach, visual-tree, render parameters, listing, visual-element modifications, and many error cases.
Project Info
MCPForUnity/Editor/Resources/Project/ProjectInfo.cs
Adds packages.uiToolkit and packages.screenCapture flags to project info output for capability detection.
Docs & Metadata
.claude/skills/unity-mcp-skill/SKILL.md, .claude/.../references/*.md, unity-mcp-skill/..., README.md, manifest.json, docs/i18n/README-zh.md
Documents and advertises new manage_ui tool, introduces UI Toolkit workflows in references/workflows, updates tools lists and manifest, and adds examples/instructions prioritizing UI Toolkit for modern Unity versions.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Server as Server
    participant Unity as UnityEditor
    participant Assets as AssetDB

    Client->>Server: manage_ui(action, params)
    Server->>Unity: send_with_unity_instance / send_mutation
    Unity->>Assets: read/write UXML/USS, PanelSettings, stylesheets
    Unity->>Unity: attach/detach UIDocument, modify VisualElements, build visual tree
    Unity->>Unity: render UI -> produce image/RenderTexture
    Unity-->>Server: result (dict with base64 contents/images if applicable)
    Server-->>Client: normalized response (decoded contents / metadata)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #301: Tool registration/auto-discovery changes that overlap with adding/registering the C# ManageUI tool.
  • PR #818: Edits to ProjectInfo/package capability fields; overlaps with added packages.uiToolkit/packages.screenCapture.
  • PR #596: Server tool registration and server tooling changes that may affect new server-side manage_ui registration and routing.

Suggested labels

codex

Suggested reviewers

  • msanatan
  • dsarno
  • justinpbarnett

Poem

🐰 I hopped through UXML and USS lines,
I stitched UIDocs and tuned the designs,
I fetched the tree and captured a scene,
Buttons now sparkle, stylesheets glean,
Hooray — a rabbit's tidy UI shines!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[Feature] New UI system' directly reflects the main change: introducing a new manage_ui tool and UI Toolkit–first workflows for Unity MCP, which is the core feature of this PR.
Description check ✅ Passed The PR description covers all required sections per the template: Description, Type of Change, Changes Made, Testing/Screenshots, and Documentation Updates. Documentation checkbox is marked as completed, fulfilling the critical requirement.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
Server/src/services/tools/manage_ui.py (2)

245-250: Inconsistent type handling between enabled and visible parameters.

enabled is passed as a boolean while visible is converted to a string ("true"/"false"). While this matches the C# implementation's expectations, it creates an inconsistent API surface. Consider documenting this behavior in the parameter annotation or normalizing both to the same type.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Server/src/services/tools/manage_ui.py` around lines 245 - 250, The code
treats enabled as a boolean but converts visible to a lowercase string, creating
an inconsistent API; update the handling in the block that populates params_dict
so both enabled and visible are normalized the same way (e.g., set
params_dict["enabled"] = str(enabled).lower() when enabled is not None and keep
params_dict["visible"] = str(visible).lower()), or alternatively add explicit
type annotations/comments on the function parameters (enabled, visible) to
document that the C# backend expects "true"/"false" strings; adjust the code
around params_dict, enabled, visible, and tooltip accordingly to keep types
consistent.

279-282: Silent exception swallowing obscures decoding failures.

The try-except-pass pattern here silently discards base64 decoding errors, making debugging difficult if malformed data is received. Per static analysis hints (S110, BLE001).

♻️ Proposed fix to log decoding errors
+import logging
+
+logger = logging.getLogger(__name__)
+
 # In the decode block:
                 try:
                     decoded = base64.b64decode(
                         data["encodedContents"]).decode("utf-8")
                     data["contents"] = decoded
                     del data["encodedContents"]
                     del data["contentsEncoded"]
-                except Exception:
-                    pass
+                except Exception as e:
+                    logger.debug("Failed to decode base64 contents: %s", e)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Server/src/services/tools/manage_ui.py` around lines 279 - 282, The code is
silently swallowing decoding errors around deleting data["encodedContents"] and
data["contentsEncoded"]; replace the bare except/pass with an explicit except
Exception as e that logs the failure (e.g., logger.error or logger.exception)
including the exception and context (the offending keys or a snippet of data) so
decoding problems are visible, then continue to preserve existing behavior;
specifically modify the try/except block that touches data["encodedContents"]
and data["contentsEncoded"] to log the error (and stacktrace if available)
instead of passing.
MCPForUnity/Editor/Tools/ManageUI.cs (1)

956-959: Canvas.ForceUpdateCanvases() is ineffective for UI Toolkit.

UI Toolkit doesn't use the Canvas system—it has its own rendering pipeline via PanelSettings and UIDocument. This call is a no-op here. The MarkDirtyRepaint() and RepaintAllViews() calls above are the appropriate methods for UI Toolkit.

♻️ Consider removing the ineffective call
                     // Mark dirty and force editor repaint so the panel renders into the RT
                     uiDoc.rootVisualElement?.MarkDirtyRepaint();
                     EditorUtility.SetDirty(panelSettings);
                     UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
-
-                    // Force a synchronous layout + repaint pass
-                    Canvas.ForceUpdateCanvases();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Tools/ManageUI.cs` around lines 956 - 959,
Canvas.ForceUpdateCanvases() is a no-op for UI Toolkit; remove the
Canvas.ForceUpdateCanvases() call and rely on the existing UI Toolkit refresh
calls (MarkDirtyRepaint() / RepaintAllViews()) instead, or replace that line
with a short comment noting UI Toolkit uses PanelSettings/UIDocument rendering
so Canvas.ForceUpdateCanvases() is unnecessary.
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageUITests.cs (1)

406-456: Consider adding tests for link_stylesheet action.

The link_stylesheet action is implemented in ManageUI.cs but not covered by tests here. Since it modifies UXML file content by injecting stylesheet references, it would benefit from test coverage.

Would you like me to generate test cases for the link_stylesheet action?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageUITests.cs`
around lines 406 - 456, Add unit tests in ManageUITests.cs covering the
ManageUI.HandleCommand "link_stylesheet" action: create a temp UXML file (use
TempRoot and ToJObject as in existing tests), call ManageUI.HandleCommand with
["action"]="link_stylesheet", ["path"]=uxmlPath,
["stylesheet"]="path/to/style.uss", assert success and that the saved UXML file
content now contains the injected stylesheet reference; add tests for
idempotency (running link twice doesn't duplicate the reference), for missing
stylesheet path returning an error, and for invalid target extension returning
the same ".uxml or .uss" error behavior; use the same patterns and helpers
(ManageUI.HandleCommand, ToJObject, TempRoot) as the surrounding tests.
unity-mcp-skill/references/tools-reference.md (1)

589-657: Mirror docs should include the full manage_ui action set.

Please mirror the same extended action coverage here (render_ui, link_stylesheet, delete, list, detach_ui_document, modify_visual_element, update_panel_settings) so both tool-reference trees stay complete and consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unity-mcp-skill/references/tools-reference.md` around lines 589 - 657, The
manage_ui doc only lists a subset of actions; extend the examples and
descriptions to include the full action set: render_ui, link_stylesheet, delete,
list, detach_ui_document, modify_visual_element, and update_panel_settings so
the tool-reference mirrors the other tree; for each action add a short Python
example call to manage_ui(action="...") showing required params and expected
return shape, plus a one-line description of behavior and any optional args,
referencing the same function name manage_ui and action strings to locate where
to insert the new examples.
.claude/skills/unity-mcp-skill/references/tools-reference.md (1)

589-657: Expand manage_ui action coverage in the reference section.

This section currently documents core actions, but PR scope includes additional actions (render_ui, link_stylesheet, delete, list, detach_ui_document, modify_visual_element, update_panel_settings). Since this file is the canonical tool reference, add at least brief examples/notes for those actions too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/unity-mcp-skill/references/tools-reference.md around lines
589 - 657, The tools reference is missing docs/examples for additional manage_ui
actions added in the PR; update the UI Tools section to include brief
examples/notes for manage_ui with actions render_ui, link_stylesheet, delete,
list, detach_ui_document, modify_visual_element, and update_panel_settings so
users know their parameters and expected return shapes. Locate the manage_ui
examples block and append concise examples for each new action showing required
keys (e.g., action="render_ui", target="UICanvas"; action="link_stylesheet",
target="UICanvas", source_asset="Assets/UI/Styles.uss"; action="delete",
path="Assets/UI/Old.uxml"; action="list", directory="Assets/UI";
action="detach_ui_document", target="UICanvas"; action="modify_visual_element",
target="UICanvas", selector={"name":"Title"}, changes={"text":"New"};
action="update_panel_settings", path="Assets/UI/Panel.asset",
scale_mode="ScaleWithScreenSize") and a one-line note about typical return
values (success/data or error) for each to match the style of the existing
create/read/update examples.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@MCPForUnity/Editor/Tools/ManageUI.cs`:
- Around line 716-733: The static play-mode capture state (s_pendingCaptureTex,
s_pendingCaptureDone, s_pendingCaptureStarted) can be corrupted by concurrent
render_ui calls; update the flow to track a unique capture token per request and
only satisfy the matching request: have render_ui generate a GUID/ID, pass it
into the MCP_ScreenCapturer instance (store it as a field on the created
GameObject/MonoBehaviour), and when Start() captures the texture set the static
state or invoke a completion callback only if the captured ID matches the
current request ID (or better, replace the shared statics with a Task/Action
stored on the MCP_ScreenCapturer instance and have Start() call that
instance-specific completion). Ensure render_ui checks/sets
s_pendingCaptureStarted atomically (or rejects new requests) and document the
single-in-flight behavior if you choose to reject concurrent captures.
- Around line 725-732: Start() calls ScreenCapture.CaptureScreenshotAsTexture()
without verifying the ScreenCapture API is available which can throw if the
module is disabled; modify Start() (the coroutine method) to first check for the
ScreenCapture availability (e.g., via Application.HasProLicense or
UnityEngine.ScreenCapture existence check / conditional compilation) or wrap the
call in a try/catch, and on failure set ManageUI.s_pendingCaptureDone = false,
ManageUI.s_pendingCaptureStarted = false (or log the error) and skip assigning
ManageUI.s_pendingCaptureTex, then Destroy(gameObject) to avoid throwing;
reference Start(), ManageUI.s_pendingCaptureTex, ManageUI.s_pendingCaptureDone,
and ManageUI.s_pendingCaptureStarted when making the change.

In `@Server/tests/integration/test_manage_ui.py`:
- Around line 188-195: The test assigns resp =
run_async(manage_ui_mod.manage_ui(...)) but never uses resp, causing an unused
variable warning; remove the unused assignment or use the result. Update the
test in test_manage_ui.py to either drop the resp variable and call
run_async(manage_ui_mod.manage_ui(...)) for its side effects, or assert
something about the returned value from manage_ui (refer to the manage_ui
function) if the response should be validated, ensuring no unused local named
resp remains.

---

Nitpick comments:
In @.claude/skills/unity-mcp-skill/references/tools-reference.md:
- Around line 589-657: The tools reference is missing docs/examples for
additional manage_ui actions added in the PR; update the UI Tools section to
include brief examples/notes for manage_ui with actions render_ui,
link_stylesheet, delete, list, detach_ui_document, modify_visual_element, and
update_panel_settings so users know their parameters and expected return shapes.
Locate the manage_ui examples block and append concise examples for each new
action showing required keys (e.g., action="render_ui", target="UICanvas";
action="link_stylesheet", target="UICanvas",
source_asset="Assets/UI/Styles.uss"; action="delete", path="Assets/UI/Old.uxml";
action="list", directory="Assets/UI"; action="detach_ui_document",
target="UICanvas"; action="modify_visual_element", target="UICanvas",
selector={"name":"Title"}, changes={"text":"New"};
action="update_panel_settings", path="Assets/UI/Panel.asset",
scale_mode="ScaleWithScreenSize") and a one-line note about typical return
values (success/data or error) for each to match the style of the existing
create/read/update examples.

In `@MCPForUnity/Editor/Tools/ManageUI.cs`:
- Around line 956-959: Canvas.ForceUpdateCanvases() is a no-op for UI Toolkit;
remove the Canvas.ForceUpdateCanvases() call and rely on the existing UI Toolkit
refresh calls (MarkDirtyRepaint() / RepaintAllViews()) instead, or replace that
line with a short comment noting UI Toolkit uses PanelSettings/UIDocument
rendering so Canvas.ForceUpdateCanvases() is unnecessary.

In `@Server/src/services/tools/manage_ui.py`:
- Around line 245-250: The code treats enabled as a boolean but converts visible
to a lowercase string, creating an inconsistent API; update the handling in the
block that populates params_dict so both enabled and visible are normalized the
same way (e.g., set params_dict["enabled"] = str(enabled).lower() when enabled
is not None and keep params_dict["visible"] = str(visible).lower()), or
alternatively add explicit type annotations/comments on the function parameters
(enabled, visible) to document that the C# backend expects "true"/"false"
strings; adjust the code around params_dict, enabled, visible, and tooltip
accordingly to keep types consistent.
- Around line 279-282: The code is silently swallowing decoding errors around
deleting data["encodedContents"] and data["contentsEncoded"]; replace the bare
except/pass with an explicit except Exception as e that logs the failure (e.g.,
logger.error or logger.exception) including the exception and context (the
offending keys or a snippet of data) so decoding problems are visible, then
continue to preserve existing behavior; specifically modify the try/except block
that touches data["encodedContents"] and data["contentsEncoded"] to log the
error (and stacktrace if available) instead of passing.

In `@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageUITests.cs`:
- Around line 406-456: Add unit tests in ManageUITests.cs covering the
ManageUI.HandleCommand "link_stylesheet" action: create a temp UXML file (use
TempRoot and ToJObject as in existing tests), call ManageUI.HandleCommand with
["action"]="link_stylesheet", ["path"]=uxmlPath,
["stylesheet"]="path/to/style.uss", assert success and that the saved UXML file
content now contains the injected stylesheet reference; add tests for
idempotency (running link twice doesn't duplicate the reference), for missing
stylesheet path returning an error, and for invalid target extension returning
the same ".uxml or .uss" error behavior; use the same patterns and helpers
(ManageUI.HandleCommand, ToJObject, TempRoot) as the surrounding tests.

In `@unity-mcp-skill/references/tools-reference.md`:
- Around line 589-657: The manage_ui doc only lists a subset of actions; extend
the examples and descriptions to include the full action set: render_ui,
link_stylesheet, delete, list, detach_ui_document, modify_visual_element, and
update_panel_settings so the tool-reference mirrors the other tree; for each
action add a short Python example call to manage_ui(action="...") showing
required params and expected return shape, plus a one-line description of
behavior and any optional args, referencing the same function name manage_ui and
action strings to locate where to insert the new examples.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca3f7bd and 57c90ba.

⛔ Files ignored due to path filters (1)
  • Server/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • .claude/skills/unity-mcp-skill/SKILL.md
  • .claude/skills/unity-mcp-skill/references/tools-reference.md
  • .claude/skills/unity-mcp-skill/references/workflows.md
  • MCPForUnity/Editor/Resources/Project/ProjectInfo.cs
  • MCPForUnity/Editor/Tools/ManageUI.cs
  • MCPForUnity/Editor/Tools/ManageUI.cs.meta
  • README.md
  • Server/src/services/tools/manage_ui.py
  • Server/tests/integration/test_manage_ui.py
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageUITests.cs
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageUITests.cs.meta
  • docs/i18n/README-zh.md
  • manifest.json
  • unity-mcp-skill/SKILL.md
  • unity-mcp-skill/references/tools-reference.md
  • unity-mcp-skill/references/workflows.md

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 16 out of 17 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +728 to +731
ManageUI.s_pendingCaptureTex = ScreenCapture.CaptureScreenshotAsTexture();
ManageUI.s_pendingCaptureDone = true;
ManageUI.s_pendingCaptureStarted = false;
Destroy(gameObject);
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The render_ui action uses ScreenCapture.CaptureScreenshotAsTexture() in play mode without checking if the ScreenCapture module is available. This could cause a runtime error if the module is disabled in Unity's Package Manager. Consider adding a check using ScreenshotUtility.IsScreenCaptureModuleAvailable before calling ScreenCapture API, or wrapping it in a try-catch with a helpful error message directing users to enable the module.

Suggested change
ManageUI.s_pendingCaptureTex = ScreenCapture.CaptureScreenshotAsTexture();
ManageUI.s_pendingCaptureDone = true;
ManageUI.s_pendingCaptureStarted = false;
Destroy(gameObject);
try
{
ManageUI.s_pendingCaptureTex = ScreenCapture.CaptureScreenshotAsTexture();
}
catch (Exception ex)
{
Debug.LogError(
"Failed to capture screenshot texture. The ScreenCapture module may be disabled in the Unity Package Manager. " +
"Please enable the ScreenCapture module to use UI rendering features.\n" + ex);
ManageUI.s_pendingCaptureTex = null;
}
finally
{
ManageUI.s_pendingCaptureDone = true;
ManageUI.s_pendingCaptureStarted = false;
Destroy(gameObject);
}

Copilot uses AI. Check for mistakes.
Comment on lines +1111 to +1112
string styleTag = $"\n <Style src=\"project://database/{stylesheetPath}\" />";
content = content.Insert(insertIdx, styleTag);
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link_stylesheet action inserts the stylesheet path directly into UXML content without XML escaping. While the path is validated by AssetPathUtility.SanitizeAssetPath (which rejects special characters including angle brackets), it would be safer to explicitly XML-escape the path when inserting it. Consider using System.Security.SecurityElement.Escape(stylesheetPath) or a similar method to prevent any potential injection if the validation logic changes in the future.

Copilot uses AI. Check for mistakes.
Comment on lines +715 to +950
private static readonly Dictionary<int, RenderTexture> s_panelRTs = new();

// Play-mode coroutine capture state
private static Texture2D s_pendingCaptureTex;
private static bool s_pendingCaptureDone;
private static bool s_pendingCaptureStarted;

// MonoBehaviour that captures a screenshot at end-of-frame in play mode.
private sealed class MCP_ScreenCapturer : MonoBehaviour
{
private System.Collections.IEnumerator Start()
{
yield return new WaitForEndOfFrame();
ManageUI.s_pendingCaptureTex = ScreenCapture.CaptureScreenshotAsTexture();
ManageUI.s_pendingCaptureDone = true;
ManageUI.s_pendingCaptureStarted = false;
Destroy(gameObject);
}
}

private static object RenderUI(JObject @params)
{
var p = new ToolParams(@params);

string target = p.Get("target");
string uxmlPath = p.Get("path");
int width = p.GetInt("width") ?? 1920;
int height = p.GetInt("height") ?? 1080;
bool includeImage = p.GetBool("include_image") || p.GetBool("includeImage");
int maxResolution = p.GetInt("max_resolution") ?? p.GetInt("maxResolution") ?? 640;
string fileName = p.Get("file_name") ?? p.Get("fileName");

if (string.IsNullOrEmpty(target) && string.IsNullOrEmpty(uxmlPath))
{
return new ErrorResponse("Either 'target' (GameObject with UIDocument) or 'path' (UXML asset path) is required.");
}

// ── Play-mode capture via ScreenCapture coroutine ──────────────────────
// PanelSettings.targetTexture is read in the same frame it is assigned,
// so the RT is always blank in a synchronous tool call. In play mode we
// dispatch a WaitForEndOfFrame coroutine that uses ScreenCapture, which
// captures the fully-composited game view (including UI Toolkit overlays).
// First call: queues the capture and returns "pending".
// Second call: result is ready – save PNG and return data.
if (Application.isPlaying)
{
// Build the output paths (used by both the pending and ready branches)
string resolvedPlayName = string.IsNullOrWhiteSpace(fileName)
? $"ui-render-{DateTime.Now:yyyyMMdd-HHmmss}.png"
: fileName.Trim();
if (!resolvedPlayName.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
resolvedPlayName += ".png";

string playFolder = Path.Combine(Application.dataPath, "Screenshots");
Directory.CreateDirectory(playFolder);
string playFullPath = Path.Combine(playFolder, resolvedPlayName).Replace('\\', '/');
playFullPath = EnsureUniqueFilePath(playFullPath);
string playAssetsRelPath = "Assets/Screenshots/" + Path.GetFileName(playFullPath);

// ── Case 1: capture is ready ──────────────────────────────────────
if (s_pendingCaptureDone && s_pendingCaptureTex != null)
{
var captureTex = s_pendingCaptureTex;
s_pendingCaptureDone = false;
s_pendingCaptureTex = null;

int captureW = captureTex.width;
int captureH = captureTex.height;
byte[] capturePng = captureTex.EncodeToPNG();
UnityEngine.Object.DestroyImmediate(captureTex);

File.WriteAllBytes(playFullPath, capturePng);
AssetDatabase.ImportAsset(playAssetsRelPath, ImportAssetOptions.ForceSynchronousImport);

var playData = new Dictionary<string, object>
{
{ "path", playAssetsRelPath },
{ "fullPath", playFullPath },
{ "width", captureW },
{ "height", captureH },
{ "hasContent", true },
};

if (!string.IsNullOrEmpty(target)) playData["gameObject"] = target;
if (!string.IsNullOrEmpty(uxmlPath)) playData["sourceAsset"] = uxmlPath;

if (includeImage)
{
int targetMax = maxResolution > 0 ? maxResolution : 640;
Texture2D downscaled = null;
try
{
var fullTex = new Texture2D(captureW, captureH, TextureFormat.RGBA32, false);
fullTex.LoadImage(capturePng);
if (captureW > targetMax || captureH > targetMax)
{
downscaled = ScreenshotUtility.DownscaleTexture(fullTex, targetMax);
playData["imageBase64"] = Convert.ToBase64String(downscaled.EncodeToPNG());
playData["imageWidth"] = downscaled.width;
playData["imageHeight"] = downscaled.height;
}
else
{
playData["imageBase64"] = Convert.ToBase64String(capturePng);
playData["imageWidth"] = captureW;
playData["imageHeight"] = captureH;
}
UnityEngine.Object.DestroyImmediate(fullTex);
}
finally
{
if (downscaled != null) UnityEngine.Object.DestroyImmediate(downscaled);
}
}

return new SuccessResponse($"UI render saved to '{playAssetsRelPath}'.", playData);
}

// ── Case 2: start a new capture ───────────────────────────────────
if (!s_pendingCaptureStarted)
{
s_pendingCaptureDone = false;
s_pendingCaptureTex = null;
s_pendingCaptureStarted = true;
var captureGo = new GameObject("__MCP_ScreenCapturer__")
{
hideFlags = HideFlags.HideAndDontSave
};
captureGo.AddComponent<MCP_ScreenCapturer>();
}

return new SuccessResponse(
"Play-mode screenshot capture queued (WaitForEndOfFrame). Call render_ui again to retrieve the rendered image.",
new Dictionary<string, object>
{
{ "pending", true },
{ "gameObject", (object)target ?? uxmlPath },
{ "note", "A screen capture was scheduled for the end of this frame. Call render_ui once more to get the result." }
});
}
// ── End play-mode branch ────────────────────────────────────────────────

// Resolve UIDocument
UIDocument uiDoc = null;
GameObject tempGo = null;
PanelSettings tempPs = null;

try
{
if (!string.IsNullOrEmpty(target))
{
var goInstruction = new JObject { ["find"] = target };
GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject;
if (go == null)
return new ErrorResponse($"Could not find target GameObject: {target}");

uiDoc = go.GetComponent<UIDocument>();
if (uiDoc == null)
return new ErrorResponse($"GameObject '{go.name}' has no UIDocument component.");
}
else
{
uxmlPath = AssetPathUtility.SanitizeAssetPath(uxmlPath);
if (uxmlPath == null)
return new ErrorResponse("Invalid UXML path.");

var vta = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath);
if (vta == null)
return new ErrorResponse($"Could not load VisualTreeAsset at: {uxmlPath}");

tempGo = new GameObject("__MCP_UI_Render_Temp__");
tempGo.hideFlags = HideFlags.HideAndDontSave;
uiDoc = tempGo.AddComponent<UIDocument>();

string[] guids = AssetDatabase.FindAssets("t:PanelSettings");
PanelSettings ps = null;
if (guids.Length > 0)
ps = AssetDatabase.LoadAssetAtPath<PanelSettings>(AssetDatabase.GUIDToAssetPath(guids[0]));
if (ps == null)
{
ps = CreateDefaultPanelSettings("Assets/UI/DefaultPanelSettings.asset");
tempPs = ps;
}

uiDoc.panelSettings = ps;
uiDoc.visualTreeAsset = vta;
}

if (uiDoc.panelSettings == null)
return new ErrorResponse("UIDocument has no PanelSettings assigned.");

var panelSettings = uiDoc.panelSettings;
int psId = panelSettings.GetInstanceID();

// Check if we already have a persistent RT assigned to this PanelSettings.
// If the RT exists and its size matches, the panel has been rendering into it.
// If not, create one and assign it — content will be available on the next call.
bool rtJustCreated = false;
RenderTexture rt = panelSettings.targetTexture as RenderTexture;

if (rt != null && s_panelRTs.ContainsKey(psId) && rt.width == width && rt.height == height)
{
// RT already assigned and size matches — panel has been rendering into it.
// We will read it below, then restore targetTexture = null so the UI
// goes back to rendering on the actual display/camera.
}
else
{
// Clean up old RT if size changed
if (s_panelRTs.TryGetValue(psId, out var oldRt) && oldRt != null)
{
panelSettings.targetTexture = null;
string oldPath = AssetDatabase.GetAssetPath(oldRt);
oldRt.Release();
if (!string.IsNullOrEmpty(oldPath))
AssetDatabase.DeleteAsset(oldPath);
else
UnityEngine.Object.DestroyImmediate(oldRt);
s_panelRTs.Remove(psId);
}

// Create RT as an asset so PanelSettings can serialize the reference properly
rt = new RenderTexture(width, height, 32, RenderTextureFormat.ARGB32);
rt.name = $"MCP_UI_Render_{psId}";
rt.Create();

string rtFolder = "Assets/UI";
if (!AssetDatabase.IsValidFolder(rtFolder))
AssetDatabase.CreateFolder("Assets", "UI");
string rtAssetPath = $"{rtFolder}/RT_MCP_UI_Render_{psId}.renderTexture";
AssetDatabase.CreateAsset(rt, rtAssetPath);
AssetDatabase.SaveAssets();

panelSettings.targetTexture = rt;
s_panelRTs[psId] = rt;
rtJustCreated = true;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The render_ui function creates persistent RenderTextures that are stored in the static s_panelRTs dictionary and reused across calls. However, there's no cleanup mechanism to release these RenderTextures when they're no longer needed (e.g., when the editor is closed or when a PanelSettings is deleted). This could lead to memory leaks over time. Consider implementing a cleanup mechanism, such as subscribing to EditorApplication.quitting or checking if PanelSettings instances are still valid before reusing cached RenderTextures.

Copilot uses AI. Check for mistakes.
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 26, 2026

Reviewer's Guide

Adds a new manage_ui MCP tool and Unity editor implementation to manage UI Toolkit (UXML/USS, UIDocument, PanelSettings, visual trees, and screenshots), exposes it through the server tool registry, and updates documentation and tests so UI Toolkit becomes the preferred UI workflow alongside legacy uGUI.

Sequence diagram for manage_ui render_ui play-mode capture

sequenceDiagram
    actor User
    participant Client as MCP_Client
    participant Server as manage_ui_server_tool
    participant Transport as Unity_transport
    participant Editor as Unity_Editor
    participant ManageUI as ManageUI_HandleCommand
    participant Capturer as MCP_ScreenCapturer

    User->>Client: Call manage_ui(action=render_ui, target=GO)
    Client->>Server: manage_ui request
    Server->>Transport: send_mutation(manage_ui, params)
    Transport->>Editor: MCP command manage_ui
    Editor->>ManageUI: HandleCommand(render_ui)
    alt first_call_in_play_mode
        ManageUI->>Capturer: Create MCP_ScreenCapturer GameObject
        ManageUI-->>Editor: Success(pending=true)
        Editor-->>Transport: response
        Transport-->>Server: response
        Server-->>Client: pending=true, note
    else second_call_after_frame
        Capturer->>Capturer: WaitForEndOfFrame
        Capturer->>ManageUI: Set pendingCaptureTex, pendingCaptureDone=true
        User->>Client: Call manage_ui(action=render_ui, target=GO) again
        Client->>Server: manage_ui request
        Server->>Transport: send_mutation(manage_ui, params)
        Transport->>Editor: MCP command manage_ui
        Editor->>ManageUI: HandleCommand(render_ui)
        ManageUI->>ManageUI: Encode PNG, save Assets/Screenshots
        ManageUI-->>Editor: Success(path, hasContent=true, optional imageBase64)
        Editor-->>Transport: response
        Transport-->>Server: response
        Server-->>Client: final screenshot info
    end
Loading

Class diagram for ManageUI editor tool structure

classDiagram
    class ManageUI {
        <<static>>
        -ValidExtensions : HashSet~string~
        -s_panelRTs : Dictionary~int, RenderTexture~
        -s_pendingCaptureTex : Texture2D
        -s_pendingCaptureDone : bool
        -s_pendingCaptureStarted : bool
        +HandleCommand(params : JObject) object
        -CreateFile(params : JObject) object
        -ReadFile(params : JObject) object
        -UpdateFile(params : JObject) object
        -DeleteFile(params : JObject) object
        -AttachUIDocument(params : JObject) object
        -DetachUIDocument(params : JObject) object
        -CreatePanelSettings(params : JObject) object
        -UpdatePanelSettings(params : JObject) object
        -ListUIAssets(params : JObject) object
        -GetVisualTree(params : JObject) object
        -ModifyVisualElement(params : JObject) object
        -RenderUI(params : JObject) object
        -LinkStylesheet(params : JObject) object
        -ValidatePath(path : string, error : out string) string
        -CreateDefaultPanelSettings(path : string) PanelSettings
        -ApplyPanelSettingsProperties(ps : PanelSettings, settings : JObject, changes : List~string~) void
        -ApplyDynamicAtlasSettings(ps : PanelSettings, da : JObject, changes : List~string~) void
        -SerializeVisualElement(element : VisualElement, depth : int, maxDepth : int) object
        -ApplyInlineStyles(element : VisualElement, styleObj : JObject, modifications : List~string~) void
        -EnsureFolderExists(assetFolderPath : string) void
        -FindUxmlBodyStart(content : string) int
        -EnsureUniqueFilePath(path : string) string
        -ColorToHex(c : Color) string
        -GetDecodedContents(p : ToolParams) string
        -TryParseEnum~T~(token : JToken, result : out T) bool
        -TryFloat(token : JToken, result : out float) bool
        -TryInt(token : JToken, result : out int) bool
        -TryParseColor(token : JToken, color : out Color) bool
        -NormalizeKey(key : string) string
    }

    class MCP_ScreenCapturer {
        +Start() IEnumerator
    }

    ManageUI ..> MCP_ScreenCapturer : creates
    ManageUI ..> UIDocument
    ManageUI ..> PanelSettings
    ManageUI ..> VisualTreeAsset
    ManageUI ..> VisualElement
    ManageUI ..> ThemeStyleSheet
    ManageUI ..> RenderTexture
Loading

File-Level Changes

Change Details Files
Introduce manage_ui Unity editor tool for UI Toolkit asset and runtime manipulation, including rendering and visual tree inspection.
  • Implement HandleCommand dispatcher supporting actions like ping, create/read/update/delete, list, attach/detach_uidocument, create/update_panel_settings, get_visual_tree, render_ui, link_stylesheet, and modify_visual_element.
  • Add path validation and base64 content handling for .uxml/.uss create/read/update/delete operations.
  • Implement PanelSettings creation/update helpers, including generic settings application and dynamic atlas settings support.
  • Add utilities to serialize VisualElement trees, inspect resolved styles, and modify live visual elements (text, classes, inline styles, enabled/visible).
  • Implement render_ui support in both editor and play mode, including RenderTexture management, ScreenCapture-based capture, PNG saving, optional downscaling, and base64 embedding.
MCPForUnity/Editor/Tools/ManageUI.cs
Expose manage_ui as an MCP tool in the server layer with validation, parameter mapping, and response post-processing.
  • Define Python manage_ui tool wrapper that validates Assets/ paths and extensions, base64-encodes contents for mutation actions, and dispatches to Unity via send_mutation or send_with_unity_instance depending on action mutability.
  • Map high-level parameters (e.g., source_asset, panel_settings, element_name, screenshot_file_name) to the C# tool’s expected casing and naming.
  • Decode base64-encoded contents returned by read into plain text in the client-visible response.
  • Mark tool as destructive and provide a detailed description of supported workflows and actions.
Server/src/services/tools/manage_ui.py
Add tests for manage_ui behavior in both Unity edit mode and the Python server layer.
  • Create Unity edit-mode tests covering HandleCommand validation, create/read/update/delete for .uxml/.uss files, PanelSettings creation, UIDocument attach/detach, list filtering, and error paths for missing/invalid inputs.
  • Add Python integration tests to verify path validation, extension constraints, contents base64 encoding, action routing between mutation and read-only paths, parameter filtering of None values, response decoding for read, and render_ui/link_stylesheet parameter wiring.
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageUITests.cs
Server/tests/integration/test_manage_ui.py
Update project capability reporting to surface UI Toolkit and screen capture support for downstream decision-making.
  • Extend ProjectInfo capabilities to always report uiToolkit=true for 2021.3+ and to include screenCapture based on ScreenshotUtility.IsScreenCaptureModuleAvailable.
MCPForUnity/Editor/Resources/Project/ProjectInfo.cs
Document UI Toolkit-first workflows and the new manage_ui tool, and wire it into skill metadata and manifests.
  • Update workflow docs to distinguish UI Toolkit vs uGUI, add detailed UI Toolkit workflow examples using manage_ui (create UXML/USS, attach UIDocument, PanelSettings, visual tree inspection), and clarify package flags including uiToolkit and screenCapture.
  • Extend tools-reference docs with a new UI Tools section describing manage_ui actions, parameters, and typical workflows, and update capability decision points to prefer UI Toolkit via manage_ui.
  • Update SKILL.md variants so the UI tool section references manage_ui for UI Toolkit and batch_execute+manage_gameobject/components for uGUI, and ensure project_info usage mentions UI Toolkit availability.
  • Add manage_ui to the tool manifest and to the English and Chinese README lists of available tools.
.claude/skills/unity-mcp-skill/references/workflows.md
.claude/skills/unity-mcp-skill/references/tools-reference.md
unity-mcp-skill/references/workflows.md
unity-mcp-skill/references/tools-reference.md
.claude/skills/unity-mcp-skill/SKILL.md
unity-mcp-skill/SKILL.md
manifest.json
README.md
docs/i18n/README-zh.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 6 issues, and left some high level feedback:

  • The ManageUI editor tool has grown quite large (file ops, panel settings, visual tree inspection, render pipeline, element mutation) – consider splitting it into smaller, focused helpers/partials (e.g., file management, panel settings, rendering, element modification) to keep it easier to reason about and evolve.
  • In the server manage_ui tool, path validation only runs for create/read/update/delete, but other actions that accept path (like link_stylesheet and render_ui with a UXML path) bypass these checks; it would be safer and more consistent to apply the same Assets/extension/traversal validation to all path-using actions.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `ManageUI` editor tool has grown quite large (file ops, panel settings, visual tree inspection, render pipeline, element mutation) – consider splitting it into smaller, focused helpers/partials (e.g., file management, panel settings, rendering, element modification) to keep it easier to reason about and evolve.
- In the server `manage_ui` tool, path validation only runs for `create/read/update/delete`, but other actions that accept `path` (like `link_stylesheet` and `render_ui` with a UXML path) bypass these checks; it would be safer and more consistent to apply the same Assets/extension/traversal validation to all path-using actions.

## Individual Comments

### Comment 1
<location path="MCPForUnity/Editor/Tools/ManageUI.cs" line_range="125-126" />
<code_context>
+                return new ErrorResponse("'contents' parameter is required for create.");
+            }
+
+            string fullPath = Path.Combine(Application.dataPath,
+                path.Substring("Assets/".Length));
+            fullPath = fullPath.Replace('/', Path.DirectorySeparatorChar);
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against non-`Assets/` paths before using `Substring("Assets/".Length)`.

Multiple methods (CreateFile, ReadFile, UpdateFile, DeleteFile, LinkStylesheet) assume `path` starts with `"Assets/"` and call `path.Substring("Assets/".Length)` without checking. Since `ValidatePath` doesn’t enforce this prefix, a value like `"UI/Main.uxml"` will throw `ArgumentOutOfRangeException`. Please either enforce and normalize the `Assets/` prefix in `ValidatePath`, or add an explicit prefix check before calling `Substring` and return a controlled error if it’s missing.
</issue_to_address>

### Comment 2
<location path="MCPForUnity/Editor/Tools/ManageUI.cs" line_range="238" />
<code_context>
+            }
+
+            // Load or create PanelSettings
+            string panelSettingsPath = p.Get("panel_settings");
+            PanelSettings panelSettings = null;
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Parameter name mismatch with Python tool for `panel_settings` will prevent explicit PanelSettings selection.

The client wrapper (`manage_ui.py`) sends `panelSettings` (`params_dict["panelSettings"] = panel_settings`), but this code only reads `panel_settings`. This means explicit PanelSettings paths are ignored and the code always uses the auto-detection/creation path. Please either support both keys (e.g. `p.Get("panel_settings") ?? p.Get("panelSettings")`) or change this parameter name to match the client.
</issue_to_address>

### Comment 3
<location path="MCPForUnity/Editor/Tools/ManageUI.cs" line_range="325-334" />
<code_context>
+                    PanelSettings ps = null;
+                    if (guids.Length > 0)
+                        ps = AssetDatabase.LoadAssetAtPath<PanelSettings>(AssetDatabase.GUIDToAssetPath(guids[0]));
+                    if (ps == null)
+                    {
+                        ps = CreateDefaultPanelSettings("Assets/UI/DefaultPanelSettings.asset");
+                        tempPs = ps;
+                    }
</code_context>
<issue_to_address>
**issue (bug_risk):** Destroying `tempPs` after creating it as an asset may leave an orphaned asset instance.

In the UXML-only `render_ui` path, `CreateDefaultPanelSettings` both creates and registers an asset, but the `finally` block calls `DestroyImmediate(tempPs, true)`. This destroys the instance while leaving the asset file behind. If this is meant to be temporary, prefer a non-asset `ScriptableObject`; if it’s meant to persist, don’t destroy it here, or explicitly remove it with `AssetDatabase.DeleteAsset` as part of cleanup.
</issue_to_address>

### Comment 4
<location path="MCPForUnity/Editor/Tools/ManageUI.cs" line_range="1474-1476" />
<code_context>
+                        }
+                        break;
+
+                    case "fontsize":
+                    case "font-size":
+                        element.style.fontSize = val.ToObject<float>();
+                        modifications.Add($"fontSize={val}");
+                        break;
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Unvalidated `ToObject<float>()` calls in inline style handling may throw for string or malformed values.

In `ApplyInlineStyles`, the direct `val.ToObject<float>()` calls (for `fontSize`, `width`, `height`, margins, etc.) will throw if the value is a string (e.g., "24" / "24px") or otherwise malformed JSON, aborting the whole change. Consider a helper like the existing `TryFloat` in `ApplyPanelSettingsProperties` and only set the style when parsing succeeds, optionally logging skipped fields in `modifications`.

Suggested implementation:

```csharp
                    case "fontsize":
                    case "font-size":
                        if (TryFloat(val, out float fontSize))
                        {
                            element.style.fontSize = fontSize;
                            modifications.Add($"fontSize={fontSize}");
                        }
                        else
                        {
                            modifications.Add($"fontSize_skipped={val}");
                        }
                        break;

```

1. Ensure that `TryFloat(JToken token, out float value)` is accessible from `ApplyInlineStyles` (e.g., it should be `private static` on the same class or otherwise visible). If it currently only exists in another helper/partial, move it to a shared location or make it available here.
2. For consistency with this change and your review comment, consider applying the same `TryFloat` pattern to other inline numeric style handlers in `ApplyInlineStyles` (e.g., `width`, `height`, margin/padding properties), including adding a `*_skipped` entry to `modifications` when parsing fails.
</issue_to_address>

### Comment 5
<location path="MCPForUnity/Editor/Tools/ManageUI.cs" line_range="1645-1654" />
<code_context>
+        {
+            bool isEncoded = p.GetBool("contents_encoded") || p.GetBool("contentsEncoded");
+
+            if (isEncoded)
+            {
+                string encoded = p.Get("encoded_contents") ?? p.Get("encodedContents");
+                if (!string.IsNullOrEmpty(encoded))
+                {
+                    try
+                    {
+                        return Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
+                    }
+                    catch (FormatException)
+                    {
+                        return null;
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Invalid base64 in `encodedContents` is treated as missing contents, which hides the real error.

The base64 decode `FormatException` in `GetDecodedContents` currently results in `null`, which then surfaces as a generic "'contents' parameter is required" error in `CreateFile`/`UpdateFile`. Consider distinguishing invalid base64 from missing contents (e.g., via a sentinel value or custom exception) so callers receive a specific "invalid encodedContents" error instead of a misleading missing-parameter message.

Suggested implementation:

```csharp
            bool isEncoded = p.GetBool("contents_encoded") || p.GetBool("contentsEncoded");

            if (isEncoded)
            {
                string encoded = p.Get("encoded_contents") ?? p.Get("encodedContents");
                if (!string.IsNullOrEmpty(encoded))
                {
                    try
                    {
                        return Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
                    }
                    catch (FormatException ex)
                    {
                        // Distinguish invalid base64 from missing contents so callers can surface
                        // a clear "invalid encodedContents" error instead of a generic missing-parameter message.
                        throw new ArgumentException(
                            "Parameter 'encodedContents' must be valid base64 when 'contentsEncoded' is true.",
                            ex);

```

Depending on how errors are propagated in your tool framework, you may want to:
1. Ensure that the exception message from `GetDecodedContents` is surfaced directly to the user (or mapped to your tool error format) rather than being wrapped in a generic error.
2. Remove or adjust any caller logic that treats `null` from `GetDecodedContents` as "missing contents" so it doesn't attempt to handle the invalid-base64 case as if it were a missing parameter.
</issue_to_address>

### Comment 6
<location path="TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageUITests.cs" line_range="406-409" />
<code_context>
+        // ---- Delete file ----
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Add tests for path traversal protection and Assets-root enforcement in editor-side file operations.

On the C# side, `ValidatePath` rejects traversal sequences and enforces `.uxml`/`.uss` extensions, but the tests only cover invalid extensions. Please add `HandleCommand` tests for `create/read/update/delete` that use traversal-like paths (e.g. `Assets/../foo.uxml`) and paths outside `Assets/`, and assert that `success` is false and the error reports an invalid/traversal path to exercise this validation.

```suggestion
        // ---- Delete file ----

        [TestCase("Assets/../Traversal_Create.uxml")]
        [TestCase("OutsideRoot/Traversal_Create.uxml")]
        public void Create_WithInvalidOrTraversalPath_FailsValidation(string path)
        {
            var result = ManageUI.HandleCommand(new JObject
            {
                ["action"] = "create",
                ["path"] = path,
                ["contents"] = "<uxml />"
            });

            Assert.IsFalse(result.Value<bool>("success"));
            Assert.That(result["error"].ToString(), Does.Contain("invalid").IgnoreCase);
        }

        [TestCase("Assets/../Traversal_Read.uxml")]
        [TestCase("OutsideRoot/Traversal_Read.uxml")]
        public void Read_WithInvalidOrTraversalPath_FailsValidation(string path)
        {
            var result = ManageUI.HandleCommand(new JObject
            {
                ["action"] = "read",
                ["path"] = path,
            });

            Assert.IsFalse(result.Value<bool>("success"));
            Assert.That(result["error"].ToString(), Does.Contain("invalid").IgnoreCase);
        }

        [TestCase("Assets/../Traversal_Update.uxml")]
        [TestCase("OutsideRoot/Traversal_Update.uxml")]
        public void Update_WithInvalidOrTraversalPath_FailsValidation(string path)
        {
            var result = ManageUI.HandleCommand(new JObject
            {
                ["action"] = "update",
                ["path"] = path,
                ["contents"] = "<uxml />"
            });

            Assert.IsFalse(result.Value<bool>("success"));
            Assert.That(result["error"].ToString(), Does.Contain("invalid").IgnoreCase);
        }

        [TestCase("Assets/../Traversal_Delete.uxml")]
        [TestCase("OutsideRoot/Traversal_Delete.uxml")]
        public void Delete_WithInvalidOrTraversalPath_FailsValidation(string path)
        {
            var result = ManageUI.HandleCommand(new JObject
            {
                ["action"] = "delete",
                ["path"] = path,
            });

            Assert.IsFalse(result.Value<bool>("success"));
            Assert.That(result["error"].ToString(), Does.Contain("invalid").IgnoreCase);
        }

        [Test]
        public void Delete_ExistingFile_DeletesFile()
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +125 to +126
string fullPath = Path.Combine(Application.dataPath,
path.Substring("Assets/".Length));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard against non-Assets/ paths before using Substring("Assets/".Length).

Multiple methods (CreateFile, ReadFile, UpdateFile, DeleteFile, LinkStylesheet) assume path starts with "Assets/" and call path.Substring("Assets/".Length) without checking. Since ValidatePath doesn’t enforce this prefix, a value like "UI/Main.uxml" will throw ArgumentOutOfRangeException. Please either enforce and normalize the Assets/ prefix in ValidatePath, or add an explicit prefix check before calling Substring and return a controlled error if it’s missing.

Comment on lines +1474 to +1476
case "fontsize":
case "font-size":
element.style.fontSize = val.ToObject<float>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Unvalidated ToObject<float>() calls in inline style handling may throw for string or malformed values.

In ApplyInlineStyles, the direct val.ToObject<float>() calls (for fontSize, width, height, margins, etc.) will throw if the value is a string (e.g., "24" / "24px") or otherwise malformed JSON, aborting the whole change. Consider a helper like the existing TryFloat in ApplyPanelSettingsProperties and only set the style when parsing succeeds, optionally logging skipped fields in modifications.

Suggested implementation:

                    case "fontsize":
                    case "font-size":
                        if (TryFloat(val, out float fontSize))
                        {
                            element.style.fontSize = fontSize;
                            modifications.Add($"fontSize={fontSize}");
                        }
                        else
                        {
                            modifications.Add($"fontSize_skipped={val}");
                        }
                        break;
  1. Ensure that TryFloat(JToken token, out float value) is accessible from ApplyInlineStyles (e.g., it should be private static on the same class or otherwise visible). If it currently only exists in another helper/partial, move it to a shared location or make it available here.
  2. For consistency with this change and your review comment, consider applying the same TryFloat pattern to other inline numeric style handlers in ApplyInlineStyles (e.g., width, height, margin/padding properties), including adding a *_skipped entry to modifications when parsing fails.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • The ManageUI editor tool has grown quite large (single 1600+ line class); consider splitting it into smaller helpers/partial classes (e.g., file ops, panel settings, visual tree/rendering, live element mutation) to keep it easier to navigate and maintain.
  • In the Python manage_ui tool, path validation is only applied for create/read/update/delete; if path is used with actions like link_stylesheet or render_ui, you may want to apply the same Assets/ + traversal checks there to fail fast before hitting Unity.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `ManageUI` editor tool has grown quite large (single 1600+ line class); consider splitting it into smaller helpers/partial classes (e.g., file ops, panel settings, visual tree/rendering, live element mutation) to keep it easier to navigate and maintain.
- In the Python `manage_ui` tool, path validation is only applied for create/read/update/delete; if `path` is used with actions like `link_stylesheet` or `render_ui`, you may want to apply the same `Assets/` + traversal checks there to fail fast before hitting Unity.

## Individual Comments

### Comment 1
<location path="MCPForUnity/Editor/Tools/ManageUI.cs" line_range="87-96" />
<code_context>
+        private static string ValidatePath(string path, out string error)
</code_context>
<issue_to_address>
**issue (bug_risk):** ValidatePath assumes an 'Assets/' prefix and can throw on other valid paths.

`ValidatePath` calls `path.Substring("Assets/".Length)` without verifying that the sanitized path actually starts with `"Assets/"`. For sanitized paths outside `Assets/` (different casing/missing prefix), this will throw `ArgumentOutOfRangeException` instead of returning a controlled error. Please add an explicit (ideally case-insensitive) `StartsWith("Assets/")` check and return a clear error before calling `Substring`.

Since this pattern appears in multiple callers (CreateFile/ReadFile/UpdateFile/DeleteFile/LinkStylesheet), centralizing the prefix validation inside `ValidatePath` would improve robustness and consistency.
</issue_to_address>

### Comment 2
<location path="MCPForUnity/Editor/Tools/ManageUI.cs" line_range="457-458" />
<code_context>
+                        if (TryFloat(val, out float fbDpi)) { ps.fallbackDpi = fbDpi; changes.Add("fallbackDpi"); }
+                        break;
+
+                    case "sortingorder":
+                        if (TryFloat(val, out float so)) { ps.sortingOrder = so; changes.Add("sortingOrder"); }
+                        break;
+
</code_context>
<issue_to_address>
**issue (bug_risk):** PanelSettings.sortingOrder is an int but is being assigned from a float helper.

`sortingorder` currently uses `TryFloat` and assigns the result to `ps.sortingOrder`, which is an `int`. This is either a compile error or an implicit narrowing conversion. Please treat this as an integer instead, consistent with `targetDisplay`:

```csharp
case "sortingorder":
    if (TryInt(val, out int so)) { ps.sortingOrder = so; changes.Add("sortingOrder"); }
    break;
```
</issue_to_address>

### Comment 3
<location path="MCPForUnity/Editor/Tools/ManageUI.cs" line_range="824-833" />
<code_context>
+
+                return new SuccessResponse(msg, data);
+            }
+            finally
+            {
+                if (tempGo != null) UnityEngine.Object.DestroyImmediate(tempGo);
+                if (tempPs != null) UnityEngine.Object.DestroyImmediate(tempPs, true);
+            }
+        }
</code_context>
<issue_to_address>
**issue (bug_risk):** Destroying a PanelSettings asset instance created on disk can leave a dangling asset.

In `RenderUI`’s `finally` block you call `DestroyImmediate(tempPs, true)` even though `tempPs` was just created via `CreateDefaultPanelSettings("Assets/UI/DefaultPanelSettings.asset")`, which writes a real asset to disk and registers it with the `AssetDatabase`.

Destroying that instance can leave the `.asset` file on disk pointing to a missing/invalid object, which may confuse the editor or future loads. Either keep the persisted default `PanelSettings` asset alive (don’t destroy it), or create a truly transient `PanelSettings` for the render path (no `AssetDatabase.CreateAsset`), and only destroy non-persisted instances.
</issue_to_address>

### Comment 4
<location path="MCPForUnity/Editor/Tools/ManageUI.cs" line_range="722-733" />
<code_context>
+            private System.Collections.IEnumerator Start()
+            {
+                yield return new WaitForEndOfFrame();
+                ManageUI.s_pendingCaptureTex = ScreenCapture.CaptureScreenshotAsTexture();
+                ManageUI.s_pendingCaptureDone = true;
+                ManageUI.s_pendingCaptureStarted = false;
</code_context>
<issue_to_address>
**suggestion:** Play-mode ScreenCapture usage isn’t guarded by the screen-capture capability flag.

`MCP_ScreenCapturer.Start()` always calls `ScreenCapture.CaptureScreenshotAsTexture()` even though you expose `screenCapture = ScreenshotUtility.IsScreenCaptureModuleAvailable` in `ProjectInfo`.

On platforms/configurations where screen capture is unavailable or disabled, this may throw or behave unpredictably while still advertising the capability.

Consider either checking `ScreenshotUtility.IsScreenCaptureModuleAvailable` (or similar) before performing the capture and failing gracefully, or only exposing `screenCapture` as true when this API is actually supported at runtime, so play-mode behavior matches the capability flag.

```suggestion
        // MonoBehaviour that captures a screenshot at end-of-frame in play mode.
        private sealed class MCP_ScreenCapturer : MonoBehaviour
        {
            private System.Collections.IEnumerator Start()
            {
                // Guard play-mode capture with the same capability used for project info,
                // so behavior matches the advertised screen-capture support.
                if (!ScreenshotUtility.IsScreenCaptureModuleAvailable)
                {
                    // Fail gracefully: mark capture as "done" with no texture
                    // and clear the "started" flag so callers can detect the failure.
                    ManageUI.s_pendingCaptureTex = null;
                    ManageUI.s_pendingCaptureDone = true;
                    ManageUI.s_pendingCaptureStarted = false;
                    Destroy(gameObject);
                    yield break;
                }

                yield return new WaitForEndOfFrame();

                ManageUI.s_pendingCaptureTex = ScreenCapture.CaptureScreenshotAsTexture();
                ManageUI.s_pendingCaptureDone = true;
                ManageUI.s_pendingCaptureStarted = false;
                Destroy(gameObject);
            }
        }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +87 to +96
private static string ValidatePath(string path, out string error)
{
error = null;
if (string.IsNullOrEmpty(path))
{
error = "'path' parameter is required.";
return null;
}

path = AssetPathUtility.SanitizeAssetPath(path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): ValidatePath assumes an 'Assets/' prefix and can throw on other valid paths.

ValidatePath calls path.Substring("Assets/".Length) without verifying that the sanitized path actually starts with "Assets/". For sanitized paths outside Assets/ (different casing/missing prefix), this will throw ArgumentOutOfRangeException instead of returning a controlled error. Please add an explicit (ideally case-insensitive) StartsWith("Assets/") check and return a clear error before calling Substring.

Since this pattern appears in multiple callers (CreateFile/ReadFile/UpdateFile/DeleteFile/LinkStylesheet), centralizing the prefix validation inside ValidatePath would improve robustness and consistency.

Add lifecycle cleanup for panel RenderTextures on editor quit / assembly reload to avoid leaked assets; implement CleanupRenderTextures. Harden play-mode screenshot flow: check ScreenshotUtility availability, catch CaptureScreenshot failures, and reject concurrent captures with a clear error. Improve error handling for decoded contents (throw/return ArgumentException message), accept panel_settings or panelSettings key, parse sortingOrder as int, and delete temporary PanelSettings assets created during operations. Robustify UXML parsing by skipping matches inside XML comments. Update tests: adjust a Python integration call and add multiple C# tests to verify path traversal validation.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
MCPForUnity/Editor/Tools/ManageUI.cs (1)

44-50: Use ToolParams for action parsing to keep validation consistent.

HandleCommand directly reads @params["action"] while the rest of the tool uses ToolParams. Please route action through ToolParams as well for consistent error behavior and type coercion patterns.

As per coding guidelines: MCPForUnity/Editor/Tools/*.cs: Use ToolParams for consistent parameter validation in C# Editor tools with methods like GetInt(), RequireString().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Tools/ManageUI.cs` around lines 44 - 50, Replace direct
JObject access in HandleCommand with ToolParams: instantiate a ToolParams from
the incoming JObject (e.g., new ToolParams(`@params`)) and call its
RequireString/GetString method to obtain the "action" value, then
ToLowerInvariant() for downstream switch logic; this keeps validation and error
behavior consistent with other tools (preserving ErrorResponse semantics) and
uses the ToolParams API instead of direct `@params`["action"] access.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@MCPForUnity/Editor/Tools/ManageUI.cs`:
- Around line 1511-1514: The code treats "[skipped]" entries from
ApplyInlineStyles as successful modifications, causing false-positive success
responses; update ManageUI to treat only real changes as modifications by either
making ApplyInlineStyles append only actual changes (not "[skipped]" messages)
or by filtering the returned modifications list to remove entries that begin
with "[skipped]" before checking modifications.Count and before setting the
response; apply the same fix for the other check around the second call (the
block referenced near lines 1661-1663) so that only true modifications count
toward success.
- Around line 988-1007: The code is deleting cached RenderTextures because after
reading the RT you set panelSettings.targetTexture = null which makes the
subsequent check treat rt as unused and remove it; instead, change the flow in
the capture path to preserve the cached RT in s_panelRTs and restore
panelSettings.targetTexture after any temporary read operation (or use a
temporary duplicate/readback RT) so that rt != null remains true across calls;
update logic around psId/s_panelRTs and the cleanup block (the branch that
checks rt, s_panelRTs.ContainsKey(psId), rt.width/height and the else that
removes oldRt) to only release/remove the cached RT when its size truly differs
or it is explicitly disposed, not just because panelSettings.targetTexture was
momentarily set to null during capture.

In `@Server/tests/integration/test_manage_ui.py`:
- Line 39: Multiple monkeypatched stub functions named fake_send in
Server/tests/integration/test_manage_ui.py declare unused named parameters and
trigger Ruff ARG001; update each fake_send signature (e.g., the one currently
defined as async def fake_send(ctx, instance, cmd, params, **kwargs): and the
other occurrences listed) to either prefix unused parameter names with an
underscore (for example _instance, _cmd, _params) or replace unused
positional/named params with *args, **kwargs so linting no longer flags ARG001
while preserving call compatibility in the tests.

---

Nitpick comments:
In `@MCPForUnity/Editor/Tools/ManageUI.cs`:
- Around line 44-50: Replace direct JObject access in HandleCommand with
ToolParams: instantiate a ToolParams from the incoming JObject (e.g., new
ToolParams(`@params`)) and call its RequireString/GetString method to obtain the
"action" value, then ToLowerInvariant() for downstream switch logic; this keeps
validation and error behavior consistent with other tools (preserving
ErrorResponse semantics) and uses the ToolParams API instead of direct
`@params`["action"] access.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57c90ba and 0a8e815.

📒 Files selected for processing (3)
  • MCPForUnity/Editor/Tools/ManageUI.cs
  • Server/tests/integration/test_manage_ui.py
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageUITests.cs

@Scriptwonder Scriptwonder merged commit 6ab1069 into CoplayDev:beta Feb 26, 2026
2 checks passed
Scriptwonder added a commit that referenced this pull request Feb 26, 2026
@Scriptwonder Scriptwonder deleted the ui branch February 26, 2026 06:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants